blob: 5ea3b0435f548022fca59228b9d1cdc10c306963 [file] [log] [blame]
#!/usr/bin/python2.6
# 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.
"""Perform various tasks related to updating Portage packages."""
import glob
import logging
import optparse
import os
import parallel_emerge
import portage
import re
import shutil
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
import chromite.lib.cros_build_lib as cros_lib
import chromite.lib.table as table
class UpgradeTable(table.Table):
"""Class to represent upgrade data in memory, can be written to csv/html."""
# These constants serve two roles, for both csv and html table output:
# 1) Restrict which column names are valid.
# 2) Specify the order of those columns.
COL_PACKAGE = 'Package'
COL_OVERLAY = 'Overlay'
COL_CURRENT_VER = 'Current Version'
COL_UPSTREAM_VER = 'Stable Upstream Version'
COL_STATE = 'State'
COL_DEPENDS_ON = 'Depends On'
COL_ACTION_TAKEN = 'Action Taken'
COLUMNS = [COL_PACKAGE,
COL_OVERLAY,
COL_CURRENT_VER,
COL_UPSTREAM_VER,
COL_STATE,
COL_DEPENDS_ON,
COL_ACTION_TAKEN,
]
# COL_STATE values should be one of the following
STATE_UNKNOWN = 'unknown'
STATE_NEEDS_UPGRADE = 'needs upgrade'
STATE_PATCHED = 'patched locally'
STATE_CURRENT = 'current'
# COL_ACTION_TAKEN values should be one of the following
ACTION_UPGRADED = 'upgraded'
ACTION_NONE = ''
def __init__(self):
table.Table.__init__(self, UpgradeTable.COLUMNS)
def WriteHTML(self, filehandle):
"""Write table out as a custom html table to |filehandle|."""
# Basic HTML, up to and including start of table and table headers.
filehandle.write('<html>\n')
filehandle.write(' <table border="1" cellspacing="0" cellpadding="3">\n')
filehandle.write(' <caption>Portage Package Status</caption>\n')
filehandle.write(' <thead>\n')
filehandle.write(' <tr>\n')
filehandle.write(' <th>%s</th>\n' %
'</th>\n <th>'.join(self._columns))
filehandle.write(' </tr>\n')
filehandle.write(' </thead>\n')
filehandle.write(' <tbody>\n')
# Now write out the rows.
for row in self._rows:
filehandle.write(' <tr>\n')
for col in self._columns:
val = row.get(col, "")
# Add color to the text in specific cases.
if val and col == self.COL_STATE:
# Add colors for state column.
if val == self.STATE_NEEDS_UPGRADE or val == self.STATE_UNKNOWN:
val = '<span style="color:red">%s</span>' % val
elif val == self.STATE_CURRENT:
val = '<span style="color:green">%s</span>' % val
if val and col == self.COL_DEPENDS_ON:
# Add colors for dependencies column. If a dependency is itself
# out of date, then make it red.
vallist = []
for cpv in val.split(' '):
# Get category/packagename from cpv, in order to look up row for
# the dependency. Then see if that pkg is red in its own row.
catpkg = Upgrader._GetCatPkgFromCpv(cpv)
deprow = self.GetRowsByValue({self.COL_PACKAGE: catpkg})[0]
if (deprow[self.COL_STATE] == self.STATE_NEEDS_UPGRADE or
deprow[self.COL_STATE] == self.STATE_UNKNOWN):
vallist.append('<span style="color:red">%s</span>' % cpv)
else:
vallist.append(cpv)
val = ' '.join(vallist)
filehandle.write(' <td>%s</td>\n' % val)
filehandle.write(' </tr>\n')
# Finish the table and html
filehandle.write(' </tbody>\n')
filehandle.write(' </table>\n')
filehandle.write('</html>\n')
class Upgrader(object):
"""A class to perform various tasks related to updating Portage packages."""
def __init__(self, options=None, args=None):
# TODO(mtennant): Don't save options object, but instead grab option
# values that are needed now. This makes mocking easier.
# For example: self._board = options.board
self._options = options
self._args = args
self._stable_repo = os.path.join(self._options.srcroot,
'third_party', 'portage-stable')
self._upstream_repo = self._options.upstream
if not self._upstream_repo:
self._upstream_repo = os.path.join(self._options.srcroot,
'third_party', 'portage')
self._table = UpgradeTable()
@staticmethod
def _GetPreOrderDepGraphPackage(deps_graph, package, pkglist, visited):
"""Collect packages from |deps_graph| into |pkglist| in pre-order."""
if package in visited:
return
visited.add(package)
for parent in deps_graph[package]['provides']:
Upgrader._GetPreOrderDepGraphPackage(deps_graph, parent, pkglist, visited)
pkglist.append(package)
@staticmethod
def _GetPreOrderDepGraph(deps_graph):
"""Return packages from |deps_graph| in pre-order."""
pkglist = []
visited = set()
for package in deps_graph:
Upgrader._GetPreOrderDepGraphPackage(deps_graph, package, pkglist,
visited)
return pkglist
@staticmethod
def _IsStableEBuild(ebuild_path):
"""Returns true if |ebuild_path| is a stable ebuild, false otherwise."""
with open(ebuild_path, 'r') as ebuild:
for line in ebuild:
if line.startswith('KEYWORDS='):
# TODO(petkov): Support non-x86 targets.
return re.search(r'[ "]x86[ "]', line)
return False
@staticmethod
def _CmpCpv(cpv1, cpv2):
"""Returns standard cmp result between |cpv1| and |cpv2|."""
return portage.versions.pkgcmp(portage.versions.pkgsplit(cpv1),
portage.versions.pkgsplit(cpv2))
@staticmethod
def _GetVerRevFromCpv(cpv):
"""Returns just the version-revision string from a full |cpv|."""
(cat, pn, version, rev) = portage.versions.catpkgsplit(cpv)
return '%s-%s' % (version, rev)
@staticmethod
def _GetCatPkgFromCpv(cpv):
"""Returns just the category/packagename string from a full |cpv|."""
(cat, pn, version, rev) = portage.versions.catpkgsplit(cpv)
return '%s/%s' % (cat, pn)
def _RunGit(self, repo, command):
"""Runs |command| in the git |repo|."""
cros_lib.RunCommand(['/bin/sh', '-c', 'cd %s && git %s' % (repo, command)],
print_cmd=self._options.verbose)
def _SplitEBuildPath(self, ebuild_path):
"""Split a full ebuild path into (overlay, cat, pn, pv)."""
(ebuild_path, ebuild) = os.path.splitext(ebuild_path)
(ebuild_path, pv) = os.path.split(ebuild_path)
(ebuild_path, pn) = os.path.split(ebuild_path)
(ebuild_path, cat) = os.path.split(ebuild_path)
(ebuild_path, overlay) = os.path.split(ebuild_path)
return (overlay, cat, pn, pv)
def _FindLatestVersion(self, cpv):
"""Returns latest cpv in |_upstream_repo| with same cat/pkg as |cpv|."""
latest_cpv = None
(cat, pn, version, rev) = portage.versions.catpkgsplit(cpv)
pkgpath = os.path.join(self._upstream_repo, cat, pn)
for ebuild_path in glob.glob(os.path.join(pkgpath, '%s*.ebuild' % pn)):
if not Upgrader._IsStableEBuild(ebuild_path): continue
(overlay, cat, pn, pv) = self._SplitEBuildPath(ebuild_path)
upstream_cpv = os.path.join(cat, pv)
if (not latest_cpv) or Upgrader._CmpCpv(upstream_cpv, latest_cpv) > 0:
latest_cpv = upstream_cpv
return latest_cpv
def _CopyUpstreamPackage(self, info):
"""Upgrades package described by |info| by copying from usptream.
Returns:
True if the packages was upgraded, False otherwise.
"""
latest_cpv = info['latest_cpv']
if not latest_cpv:
return False
(cat, pkgname, version, rev) = portage.versions.catpkgsplit(latest_cpv)
catpkgname = os.path.join(cat, pkgname)
pkgdir = os.path.join(self._stable_repo, catpkgname)
if os.path.exists(pkgdir):
shutil.rmtree(pkgdir)
upstream_pkgdir = os.path.join(self._upstream_repo, cat, pkgname)
# Copy the whole package except the ebuilds.
shutil.copytree(upstream_pkgdir, pkgdir,
ignore=shutil.ignore_patterns('*.ebuild'))
# Copy just the ebuild that will be used in the build.
shutil.copy2(os.path.join(upstream_pkgdir,
latest_cpv.split('/')[1] + '.ebuild'), pkgdir)
self._RunGit(self._stable_repo, 'add ' + catpkgname)
return True
def _UpgradePackage(self, info):
"""Updates |info| with latest_cpv, performs upgrade if applicable.
The upgrade is performed only if the package is outdated and --upgrade
is specified.
"""
cpv = info['cpv']
info['latest_cpv'] = self._FindLatestVersion(cpv)
cpv_cmp_upstream = None
info['upgraded'] = False
if info['latest_cpv']:
# cpv_cmp_upstream values: 0 = current, >0 = outdated, <0 = futuristic!
cpv_cmp_upstream = Upgrader._CmpCpv(info['latest_cpv'], cpv)
if (self._options.upgrade and info['overlay'].startswith('portage') and
cpv_cmp_upstream > 0):
info['upgraded'] = self._CopyUpstreamPackage(info)
# Print details for this package
if cpv_cmp_upstream is None:
state = UpgradeTable.STATE_UNKNOWN
elif cpv_cmp_upstream > 0:
# Upstream upgrade available. Note that it is possible that the local
# package has also been patched.
# TODO(mtennant): A check for when package is locally patched and
# uprev'ed upstream.
state = UpgradeTable.STATE_NEEDS_UPGRADE
elif cpv_cmp_upstream < 0:
# Local package is patched from upstream package
state = UpgradeTable.STATE_PATCHED
else:
state = UpgradeTable.STATE_CURRENT
action_taken = UpgradeTable.ACTION_NONE
if info['upgraded']: action_taken = UpgradeTable.ACTION_UPGRADED
# Print a brief one-line report
if action_taken == UpgradeTable.ACTION_UPGRADED:
action_stat = ' (UPGRADED)'
else:
action_stat = ''
up_stat = {UpgradeTable.STATE_UNKNOWN: ' no stable package found upstream!',
UpgradeTable.STATE_NEEDS_UPGRADE: ' -> %s' % info['latest_cpv'],
UpgradeTable.STATE_PATCHED: ' <- %s' % info['latest_cpv'],
UpgradeTable.STATE_CURRENT: ' (current)',
}[state]
print '[%s] %s%s%s' % (info['overlay'], info['cpv'],
up_stat, action_stat)
# Create a table row to represent this package.
# Recreate list of packages that this cpv depends on.
needslist = self._deps_graph[cpv]['needs'].keys()
upstream_ver = 'N/A'
if info['latest_cpv']:
upstream_ver = Upgrader._GetVerRevFromCpv(info['latest_cpv'])
# Translate to table column values:
self._table.AppendRow({UpgradeTable.COL_PACKAGE: info['package'],
UpgradeTable.COL_OVERLAY: info['overlay'],
UpgradeTable.COL_CURRENT_VER: info['version_rev'],
UpgradeTable.COL_UPSTREAM_VER: upstream_ver,
UpgradeTable.COL_STATE: state,
UpgradeTable.COL_DEPENDS_ON: ' '.join(needslist),
UpgradeTable.COL_ACTION_TAKEN: action_taken,
})
def _OpenFileForWrite(self, file):
"""If |file| not None, open for writing."""
try:
if file:
return open(file, 'w')
except IOError as ex:
print("Unable to open %s for write: %s" % (file, str(ex)))
return None
def _WriteTableFiles(self, csv=None, html=None):
"""Write table to |csv| and/or |html| files, if requested."""
# First sort the table, putting packages of greater interest toward the top.
def rowkey(row):
# First key is for COL_STATE value.
state_val = row[UpgradeTable.COL_STATE]
key1 = {UpgradeTable.STATE_NEEDS_UPGRADE: 0,
UpgradeTable.STATE_UNKNOWN: 1,
UpgradeTable.STATE_PATCHED: 2,
}.get(state_val, 4)
# Next key is for overlay.
key2 = row[UpgradeTable.COL_OVERLAY]
# Next key is for package name, which is unique final decider.
key3 = row[UpgradeTable.COL_PACKAGE]
return (key1, key2, key3)
self._table.Sort(rowkey)
if csv:
filehandle = self._OpenFileForWrite(csv)
if filehandle:
self._table.WriteCSV(filehandle)
filehandle.close()
if html:
filehandle = self._OpenFileForWrite(html)
if filehandle:
self._table.WriteHTML(filehandle)
filehandle.close()
def _UpgradePackages(self, infolist):
"""Given a list of cpv info maps, adds the latest_cpv to the infos."""
self._table.Clear()
dash_q = ''
if not self._options.verbose: dash_q = '-q'
try:
# TODO(petkov): Currently portage's master branch is stale so we need to
# checkout latest upstream. At some point portage's master branch will be
# upstream so there will be no need to chdir/checkout. At that point we
# can also fuse this loop into the caller and avoid generating a separate
# list.
if not self._options.upstream:
self._RunGit(self._upstream_repo, 'checkout %s cros/gentoo' % dash_q)
message = ''
for info in infolist:
self._UpgradePackage(info)
if info['upgraded']:
message += 'Upgrade %s to %s\n' % (info['cpv'], info['latest_cpv'])
if message:
message = 'Upgrade Portage packages\n\n' + message
message += '\nBUG=<fill-in>'
message += '\nTEST=<fill-in>'
self._RunGit(self._stable_repo, "commit -am '%s'" % message)
cros_lib.Info('Use "git commit --amend" to update the commit message.')
finally:
if not self._options.upstream:
self._RunGit(self._upstream_repo, 'checkout %s cros/master' % dash_q)
def _GenParallelEmergeArgv(self):
"""Creates an argv for parallel_emerge based on current options."""
argv = ['--emptytree', '--pretend']
if self._options.board:
argv.append('--board=%s' % self._options.board)
if not self._options.verbose:
argv.append('--quiet')
if self._options.rdeps:
argv.append('--root-deps=rdeps')
argv.extend(self._args)
return argv
def _GetCurrentVersions(self):
"""Returns a list of cpvs of the current package dependencies.
The returned list is ordered such that the dependencies of any mentioned
cpv occur later in the list."""
argv = self._GenParallelEmergeArgv()
deps = parallel_emerge.DepGraphGenerator()
deps.Initialize(argv)
deps_tree, deps_info = deps.GenDependencyTree()
self._deps_graph = deps.GenDependencyGraph(deps_tree, deps_info)
return Upgrader._GetPreOrderDepGraph(self._deps_graph)
def _GetInfoListWithOverlays(self, cpvlist):
"""Returns a list of cpv/overlay info maps corresponding to |cpvlist|."""
infolist = []
for cpv in cpvlist:
# No need to report or try to upgrade chromeos-base packages.
if cpv.startswith('chromeos-base/'): continue
# TODO(petkov): Use internal portage utilities to find the overlay instead
# of equery to improve performance, if possible.
equery = ['equery', 'which', cpv]
if self._options.board:
equery[0] = 'equery-%s' % self._options.board
ebuild_path = cros_lib.RunCommand(equery, print_cmd=self._options.verbose,
redirect_stdout=True).output.strip()
(overlay, cat, pn, pv) = self._SplitEBuildPath(ebuild_path)
ver_rev = pv.replace(pn + '-', '')
infolist.append({'cpv': cpv, 'overlay': overlay,
'package': '%s/%s' % (cat, pn), 'version_rev': ver_rev,
'category': cat, 'package_name': pn, 'package_ver': pv})
return infolist
def Run(self):
"""Runs the upgrader based on the supplied options and arguments.
Currently just lists all package dependencies in pre-order along with
potential upgrades."""
cpvlist = self._GetCurrentVersions()
infolist = self._GetInfoListWithOverlays(cpvlist)
self._UpgradePackages(infolist)
self._WriteTableFiles(csv=self._options.csv_file,
html=self._options.html_file)
def main():
usage = 'Usage: %prog [options] packages...'
parser = optparse.OptionParser(usage=usage)
parser.add_option('--board', dest='board', type='string', action='store',
default=None, help="Target board [default: '%default']")
parser.add_option('--rdeps', dest='rdeps', action='store_true',
default=False,
help="Use runtime dependencies only")
parser.add_option('--srcroot', dest='srcroot', type='string', action='store',
default='%s/trunk/src' % os.environ['HOME'],
help="Path to root src directory [default: '%default']")
parser.add_option('--to_csv', dest='csv_file', type='string', action='store',
default=None, help="File to write csv-formatted results to")
parser.add_option('--to_html', dest='html_file',
type='string', action='store', default=None,
help="File to write html-formatted results to")
parser.add_option('--upgrade', dest='upgrade', action='store_true',
default=False,
help="Perform package upgrade")
parser.add_option('--upstream', dest='upstream', type='string',
action='store', default=None,
help="Latest upstream repo location [default: '%default']")
parser.add_option('--verbose', dest='verbose', action='store_true',
default=False,
help="Enable verbose output (for debugging)")
(options, args) = parser.parse_args()
if (options.verbose): logging.basicConfig(level=logging.DEBUG)
# Only the unit tests are allowed to not have the board option set.
if not options.board:
parser.print_help()
cros_lib.Die('board is required')
if not args:
parser.print_help()
cros_lib.Die('no packages provided')
# If upstream portage is provided, verify that it is a valid directory.
if options.upstream and not os.path.isdir(options.upstream):
parser.print_help()
cros_lib.Die('argument to --upstream must be a valid directory')
upgrader = Upgrader(options, args)
upgrader.Run()
if __name__ == '__main__':
main()