| #!/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 logging |
| import optparse |
| import os |
| import parallel_emerge |
| import portage |
| import re |
| import shutil |
| import sys |
| import tempfile |
| |
| 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 |
| |
| # TODO(mtennant): I see comments next to cros_build_lib.Info,Warning,Die that |
| # they are deprecated and to be replaced by those in operation module. |
| |
| class UpgradeTable(table.Table): |
| """Class to represent upgrade data in memory, can be written to csv/html.""" |
| |
| # Column names. Note that 'ARCH' is replaced with a real arch name when |
| # these are accessed as attributes off an UpgradeTable object. |
| COL_PACKAGE = 'Package' |
| COL_SLOT = 'Slot' |
| COL_OVERLAY = 'Overlay' |
| COL_CURRENT_VER = 'Current ARCH Version' |
| COL_STABLE_UPSTREAM_VER = 'Stable Upstream ARCH Version' |
| COL_LATEST_UPSTREAM_VER = 'Latest Upstream ARCH Version' |
| COL_STATE = 'State On ARCH' |
| COL_DEPENDS_ON = 'Dependencies On ARCH' |
| COL_TARGET = 'Root Target' |
| COL_ACTION_TAKEN = 'Action Taken' |
| |
| # COL_STATE values should be one of the following: |
| STATE_UNKNOWN = 'unknown' |
| STATE_NEEDS_UPGRADE = 'needs upgrade' |
| STATE_PATCHED = 'patched locally' |
| STATE_DUPLICATED = 'duplicated locally' |
| STATE_NEEDS_UPGRADE_AND_PATCHED = 'needs upgrade and patched locally' |
| STATE_NEEDS_UPGRADE_AND_DUPLICATED = 'needs upgrade and duplicated locally' |
| STATE_CURRENT = 'current' |
| |
| @staticmethod |
| def GetColumnName(col, arch=None): |
| """Translate from generic column name to specific given |arch|.""" |
| if arch: |
| return col.replace('ARCH', arch) |
| |
| def __init__(self, arch): |
| self._arch = arch |
| |
| # 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. |
| columns = [self.COL_PACKAGE, |
| self.COL_SLOT, |
| self.COL_OVERLAY, |
| self.COL_CURRENT_VER, |
| self.COL_STABLE_UPSTREAM_VER, |
| self.COL_LATEST_UPSTREAM_VER, |
| self.COL_STATE, |
| self.COL_DEPENDS_ON, |
| self.COL_TARGET, |
| self.COL_ACTION_TAKEN, |
| ] |
| |
| table.Table.__init__(self, columns) |
| |
| def __getattribute__(self, name): |
| """When accessing self.COL_*, substitute ARCH name.""" |
| if name.startswith('COL_'): |
| text = getattr(UpgradeTable, name) |
| return UpgradeTable.GetColumnName(text, arch=self._arch) |
| else: |
| return object.__getattribute__(self, name) |
| |
| 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_NEEDS_UPGRADE_AND_DUPLICATED or |
| val == self.STATE_NEEDS_UPGRADE_AND_PATCHED): |
| 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.""" |
| |
| UPSTREAM_OVERLAY_NAME = 'portage' |
| STABLE_OVERLAY_NAME = 'portage-stable' |
| CROS_OVERLAY_NAME = 'chromiumos-overlay' |
| HOST_BOARD = 'amd64-host' |
| OPT_SLOTS = ['amend', 'board', 'csv_file', 'html_file', 'rdeps', |
| 'stable_only', 'upgrade', 'upgrade_deep', 'upstream', 'verbose'] |
| |
| __slots__ = ['_amend', # Boolean to use --amend with upgrade commit |
| '_arch', # Architecture for current board |
| '_args', # Commandline arguments (portage targets) |
| '_board', # Board for current run |
| '_cros_overlay', # Path to chromiumos-overlay repo |
| '_csv_file', # File path for writing csv output |
| '_deps_graph', # Dependency graph from portage |
| '_emptydir', # Path to temporary empty directory |
| '_html_file', # File path for writing html output |
| '_porttree', # Reference to portage porttree object |
| '_rdeps', # Boolean, if True pass --root-deps=rdeps |
| '_stable_only', # Boolean to require only stable upstream |
| '_stable_repo', # Path to portage-stable |
| '_table', # chromite.lib.table holding package status |
| '_upgrade', # Boolean indicating upgrade requested |
| '_upgrade_deep', # Boolean indicating upgrade_deep requested |
| '_upstream', # User-provided path to upstream repo |
| '_upstream_repo',# Path to upstream portage repo |
| '_verbose', # Boolean |
| ] |
| |
| def __init__(self, options=None, args=None): |
| self._args = args |
| |
| self._stable_repo = os.path.join(options.srcroot, 'third_party', |
| self.STABLE_OVERLAY_NAME) |
| self._upstream_repo = options.upstream |
| if not self._upstream_repo: |
| self._upstream_repo = os.path.join(options.srcroot, 'third_party', |
| self.UPSTREAM_OVERLAY_NAME) |
| self._cros_overlay = os.path.join(options.srcroot, 'third_party', |
| self.CROS_OVERLAY_NAME) |
| |
| # Save options needed later. |
| for opt in self.OPT_SLOTS: |
| setattr(self, '_' + opt, getattr(options, opt, None)) |
| |
| # TODO(mtennant): Attributes below are specific to one run with a specific |
| # board and should be cleared before each run when multiple boards are |
| # supported at once. |
| self._board = options.board |
| self._arch = Upgrader._FindBoardArch(self._board) |
| self._table = UpgradeTable(self._arch) |
| |
| self._porttree = None |
| self._emptydir = None |
| self._deps_graph = None |
| |
| def _GetPkgKeywordsFile(self): |
| """Return the path to the package.keywords file in chromiumos-overlay.""" |
| return '%s/profiles/targets/chromeos/package.keywords' % self._cros_overlay |
| |
| def _CheckStableRepoOnBranch(self): |
| """Raise exception if stable repo is not on a branch now.""" |
| result = self._RunGit(self._stable_repo, 'branch', redirect_stdout=True) |
| if result.returncode == 0: |
| for line in result.output.split('\n'): |
| match = re.search(r'^\*\s+(.+)$', line) |
| if match: |
| # Found current branch, see if it is a real branch. |
| branch = match.group(1) |
| if branch != '(no branch)': |
| return |
| raise RuntimeError("To perform upgrade, %s must be on a branch." % |
| self._stable_repo) |
| |
| raise RuntimeError("Unable to determine whether %s is on a branch." % |
| self._stable_repo) |
| |
| def _PkgUpgradeRequested(self, info): |
| """Return True if upgrade of pkg in |info| hash was requested by user.""" |
| if self._upgrade_deep: |
| return True |
| |
| if self._upgrade: |
| # See if this package was directly requested at command line. |
| for pkg in self._args: |
| if pkg == info['package'] or pkg == info['package_name']: |
| return True |
| |
| return False |
| |
| @staticmethod |
| def _FindBoardArch(board): |
| """Return the architecture for a given board name.""" |
| # Host is a special case |
| if board == Upgrader.HOST_BOARD: |
| return 'amd64' |
| |
| # Leverage Portage 'portageq' tool to do this. |
| cmd = ['portageq-%s' % board, 'envvar', 'ARCH'] |
| cmd_result = cros_lib.RunCommand(cmd, print_cmd=False, |
| redirect_stdout=True, exit_code=True) |
| if cmd_result.returncode == 0: |
| return cmd_result.output.strip() |
| else: |
| return None |
| |
| @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 _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|.""" |
| if not cpv: |
| return None |
| |
| (cat, pn, version, rev) = portage.versions.catpkgsplit(cpv) |
| if rev != 'r0': |
| return '%s-%s' % (version, rev) |
| else: |
| return version |
| |
| @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, redirect_stdout=False): |
| """Runs |command| in the git |repo|. |
| |
| This leverages the cros_build_lib.RunCommand function. The |
| |redirect_stdout| argument is passed to that function. |
| |
| Returns a Result object as documented by cros_build_lib.RunCommand. |
| Most usefully, the result object has a .output attribute containing |
| the output from the command (if |redirect_stdout| was True). |
| """ |
| cmdline = ['/bin/sh', '-c', 'cd %s && git -c core.pager=" " %s' % |
| (repo, command)] |
| result = cros_lib.RunCommand(cmdline, exit_code=True, |
| print_cmd=self._verbose, |
| redirect_stdout=redirect_stdout) |
| return result |
| |
| 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 _GenPortageEnvvars(self, arch, unstable_ok, portdir=None, |
| portage_configroot=None): |
| """Returns dictionary of envvars for running portage tools. |
| |
| If |arch| is set, then ACCEPT_KEYWORDS will be included and set |
| according to |unstable_ok|. |
| |
| PORTDIR is set to |portdir| value, if not None. |
| PORTAGE_CONFIGROOT is set to |portage_configroot| value, if not None. |
| """ |
| envvars = {} |
| if arch: |
| if unstable_ok: |
| envvars['ACCEPT_KEYWORDS'] = arch + ' ~' + arch |
| else: |
| envvars['ACCEPT_KEYWORDS'] = arch |
| |
| if portdir is not None: |
| envvars['PORTDIR'] = portdir |
| if portage_configroot is not None: |
| envvars['PORTAGE_CONFIGROOT'] = portage_configroot |
| |
| return envvars |
| |
| def _FindUpstreamCPV(self, pkg, arch, unstable_ok=False): |
| """Returns latest cpv in |_upstream_repo| that matches |pkg|. |
| |
| The |pkg| argument can specify as much or as little of the full CPV |
| syntax as desired, exactly as accepted by the Portage 'equery' command. |
| To find whether an exact version exists upstream specify the full |
| CPV. To find the latest version specify just the category and package |
| name. |
| |
| To filter by architecture keyword (e.g. 'arm' or 'x86'), specify |
| the |arch| argument. By default, only ebuilds stable on that arch |
| will be accepted. To accept unstable ebuilds, set |unstable_ok| to True. |
| |
| Returns upstream cpv, if found. |
| """ |
| envvars = self._GenPortageEnvvars(arch, unstable_ok, |
| portdir=self._upstream_repo, |
| portage_configroot=self._emptydir) |
| |
| # Point equery to the upstream source to get latest version for keywords. |
| equery = ['equery', 'which', pkg ] |
| cmd_result = cros_lib.RunCommand(equery, extra_env=envvars, |
| print_cmd=self._verbose, |
| exit_code=True, error_ok=True, |
| redirect_stdout=True, |
| combine_stdout_stderr=True, |
| ) |
| if cmd_result.returncode == 0: |
| ebuild_path = cmd_result.output.strip() |
| (overlay, cat, pn, pv) = self._SplitEBuildPath(ebuild_path) |
| return os.path.join(cat, pv) |
| else: |
| return None |
| |
| def _IsEmergeable(self, cpv, stable_only): |
| """Indicate whether |cpv| can be emerged on current board. |
| |
| This essentially runs emerge with the --pretend option to verify |
| that all dependencies for this package version are satisfied. |
| |
| The |stable_only| argument determines whether an unstable version |
| is acceptable. |
| |
| Return tuple with two elements: |
| [0] True if |cpv| can be emerged. |
| [1] Output from the emerge command. |
| """ |
| envvars = self._GenPortageEnvvars(self._arch, not stable_only) |
| |
| emerge = 'emerge' |
| if self._board != self.HOST_BOARD: |
| emerge = 'emerge-%s' % self._board |
| |
| cmd = [emerge, '-p', '=' + cpv] |
| result = cros_lib.RunCommand(cmd, exit_code=True, error_ok=True, |
| extra_env=envvars, print_cmd=False, |
| redirect_stdout=True, |
| combine_stdout_stderr=True, |
| ) |
| |
| return (result.returncode == 0, result.output) |
| |
| def _VerifyEbuildOverlay(self, cpv, overlay, stable_only): |
| """Raises exception if ebuild for |cpv| is not from |overlay|. |
| |
| Essentially, this verifies that the upgraded ebuild in portage-stable |
| is indeed the one being picked up, rather than some other ebuild with |
| the same version in another overlay. |
| """ |
| # Further explanation: this check should always pass, but might not |
| # if the copy/upgrade from upstream did not work and |
| # src/third-party/portage is being used as temporary upstream copy via |
| # 'git checkout cros/gentoo'. This is just a sanity check. |
| envvars = self._GenPortageEnvvars(self._arch, not stable_only) |
| |
| equery = 'equery' |
| if self._board != self.HOST_BOARD: |
| equery = 'equery-%s' % self._board |
| |
| cmd = [equery, 'which', cpv] |
| result = cros_lib.RunCommand(cmd, exit_code=True, error_ok=True, |
| extra_env=envvars, print_cmd=False, |
| redirect_stdout=True, |
| combine_stdout_stderr=True, |
| ) |
| ebuild_path = result.output.strip() |
| (ovrly, cat, pn, pv) = self._SplitEBuildPath(ebuild_path) |
| if ovrly != overlay: |
| raise RuntimeError('Somehow ebuild for %s is not coming from %s:\n %s' % |
| (cpv, overlay, ebuild_path)) |
| |
| def _CopyUpstreamPackage(self, upstream_cpv): |
| """Upgrades package in |upstream_cpv| to the version in |upstream_cpv|. |
| |
| Returns: |
| The upstream_cpv if the package was upgraded, None otherwise. |
| """ |
| if not upstream_cpv: |
| return None |
| |
| (cat, pkgname, version, rev) = portage.versions.catpkgsplit(upstream_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, |
| upstream_cpv.split('/')[1] + '.ebuild'), pkgdir) |
| self._RunGit(self._stable_repo, 'add ' + catpkgname) |
| return upstream_cpv |
| |
| def _GetPackageUpgradeState(self, info, cpv_cmp_upstream): |
| """Return state value for package in |info| given |cpv_cmp_upstream|. |
| |
| The value in |cpv_cmp_upstream| represents a comparison of cpv version |
| and the upstream version, where: |
| 0 = current, >0 = outdated, <0 = futuristic! |
| """ |
| # See whether this specific cpv exists upstream. |
| cpv = info['cpv'] |
| cpv_exists_upstream = bool(self._FindUpstreamCPV(cpv, self._arch, |
| unstable_ok=True)) |
| |
| # Convention is that anything not in portage overlay has been altered. |
| # TODO(mtennant): Distinguish between 'portage' and 'portage-stable' |
| # overlays in status reports. Use something like: |
| # locally_pinned = overlay == self.STABLE_OVERLAY_NAME |
| overlay = info['overlay'] |
| locally_patched = (overlay != self.UPSTREAM_OVERLAY_NAME and |
| overlay != self.STABLE_OVERLAY_NAME) |
| locally_duplicated = locally_patched and cpv_exists_upstream |
| |
| # Gather status details for this package |
| if cpv_cmp_upstream is None: |
| state = UpgradeTable.STATE_UNKNOWN |
| elif cpv_cmp_upstream > 0: |
| if locally_duplicated: |
| state = UpgradeTable.STATE_NEEDS_UPGRADE_AND_DUPLICATED |
| elif locally_patched: |
| state = UpgradeTable.STATE_NEEDS_UPGRADE_AND_PATCHED |
| else: |
| state = UpgradeTable.STATE_NEEDS_UPGRADE |
| elif locally_duplicated: |
| state = UpgradeTable.STATE_DUPLICATED |
| elif locally_patched: |
| state = UpgradeTable.STATE_PATCHED |
| else: |
| state = UpgradeTable.STATE_CURRENT |
| |
| return state |
| |
| def _PrintPackageLine(self, info): |
| """Print a brief one-line report of package status.""" |
| upstream_cpv = info['upstream_cpv'] |
| if info['upgraded_cpv']: |
| if info['emerge_ok']: |
| action_stat = ' (UPGRADED, EMERGE WORKS)' |
| else: |
| action_stat = ' (UPGRADED, BUT EMERGE FAILS)' |
| else: |
| action_stat = '' |
| up_stat = {UpgradeTable.STATE_UNKNOWN: ' no package found upstream!', |
| UpgradeTable.STATE_NEEDS_UPGRADE: ' -> %s' % upstream_cpv, |
| UpgradeTable.STATE_NEEDS_UPGRADE_AND_PATCHED: |
| ' <-> %s' % upstream_cpv, |
| UpgradeTable.STATE_NEEDS_UPGRADE_AND_DUPLICATED: |
| ' (locally duplicated) <-> %s' % upstream_cpv, |
| UpgradeTable.STATE_PATCHED: ' <- %s' % upstream_cpv, |
| UpgradeTable.STATE_DUPLICATED: ' (locally duplicated)', |
| UpgradeTable.STATE_CURRENT: ' (current)', |
| }[info['state']] |
| print '[%s] %s%s%s' % (info['overlay'], info['cpv'], |
| up_stat, action_stat) |
| |
| def _AppendPackageRow(self, info): |
| """Add a row to status table for the package in |info|.""" |
| cpv = info['cpv'] |
| upstream_cpv = info['upstream_cpv'] |
| upstream_ver = Upgrader._GetVerRevFromCpv(upstream_cpv) |
| upgraded_cpv = info['upgraded_cpv'] |
| |
| # Prepare defaults for columns without values for this row. |
| action_taken = '' |
| if upgraded_cpv: |
| if info['emerge_ok']: |
| action_taken = 'upgraded to %s' % upstream_ver |
| else: |
| action_taken = 'upgraded to %s (but emerge fails)' % upstream_ver |
| depslist = sorted(self._deps_graph[cpv]['needs'].keys()) # dependencies |
| stable_up_ver = Upgrader._GetVerRevFromCpv(info['stable_upstream_cpv']) |
| if not stable_up_ver: |
| stable_up_ver = 'N/A' |
| latest_up_ver = Upgrader._GetVerRevFromCpv(info['latest_upstream_cpv']) |
| if not latest_up_ver: |
| latest_up_ver = 'N/A' |
| row = {self._table.COL_PACKAGE: info['package'], |
| self._table.COL_SLOT: info['slot'], |
| self._table.COL_OVERLAY: info['overlay'], |
| self._table.COL_CURRENT_VER: info['version_rev'], |
| self._table.COL_STABLE_UPSTREAM_VER: stable_up_ver, |
| self._table.COL_LATEST_UPSTREAM_VER: latest_up_ver, |
| self._table.COL_STATE: info['state'], |
| self._table.COL_DEPENDS_ON: ' '.join(depslist), |
| self._table.COL_TARGET: ' '.join(self._args), |
| self._table.COL_ACTION_TAKEN: action_taken, |
| } |
| self._table.AppendRow(row) |
| |
| def _UpgradePackage(self, info): |
| """Gathers upgrade status for pkg, performs upgrade if requested. |
| |
| The upgrade is performed only if the package is outdated and --upgrade |
| is specified. |
| |
| Regardless, the following entries in |info| dict are filled in: |
| stable_upstream_cpv |
| latest_upstream_cpv |
| upstream_cpv (one of the above, depending on --stable-only option) |
| upgrade_cpv (if upgrade performed) |
| """ |
| cpv = info['cpv'] |
| catpkg = Upgrader._GetCatPkgFromCpv(cpv) |
| info['stable_upstream_cpv'] = self._FindUpstreamCPV(catpkg, self._arch) |
| info['latest_upstream_cpv'] = self._FindUpstreamCPV(catpkg, self._arch, |
| unstable_ok=True) |
| |
| # The upstream version can be either latest stable or latest overall. |
| if self._stable_only: |
| upstream_cpv = info['stable_upstream_cpv'] |
| else: |
| upstream_cpv = info['latest_upstream_cpv'] |
| info['upstream_cpv'] = upstream_cpv |
| |
| # Perform the actual upgrade, if requested. |
| cpv_cmp_upstream = None |
| info['upgraded_cpv'] = False |
| if upstream_cpv: |
| # cpv_cmp_upstream values: 0 = current, >0 = outdated, <0 = futuristic! |
| cpv_cmp_upstream = Upgrader._CmpCpv(upstream_cpv, cpv) |
| |
| # Determine whether upgrade of this package is requested. |
| if cpv_cmp_upstream > 0 and self._PkgUpgradeRequested(info): |
| info['upgraded_cpv'] = self._CopyUpstreamPackage(upstream_cpv) |
| |
| # Verify that upgraded package can be emerged and save results. |
| # Prefer stable if possible, otherwise remember that a keyword |
| # change will be needed. |
| (em_ok_stable, em_out_stable) = self._IsEmergeable(upstream_cpv, True) |
| (em_ok_all, em_out_all) = self._IsEmergeable(upstream_cpv, False) |
| if em_ok_stable or not em_ok_all: |
| info['emerge_ok'] = em_ok_stable |
| info['emerge_output'] = em_out_stable |
| info['emerge_stable'] = True |
| else: |
| info['emerge_ok'] = em_ok_all |
| info['emerge_output'] = em_out_all |
| info['emerge_stable'] = False |
| if info['emerge_ok']: |
| self._VerifyEbuildOverlay(upstream_cpv, self.STABLE_OVERLAY_NAME, |
| info['emerge_stable']) |
| |
| info['state'] = self._GetPackageUpgradeState(info, cpv_cmp_upstream) |
| |
| # Print a quick summary of package status. |
| self._PrintPackageLine(info) |
| |
| # Add a row to status table for this package |
| self._AppendPackageRow(info) |
| |
| def _OpenFileForWrite(self, filepath): |
| """If |file| not None, open for writing.""" |
| try: |
| if filepath: |
| return open(filepath, 'w') |
| except IOError as ex: |
| print("Unable to open %s for write: %s" % (filepath, str(ex))) |
| return None |
| |
| def _WriteTableFiles(self, csv=None, html=None): |
| """Write table to |csv| and/or |html| files, if requested.""" |
| # Sort the table by package name, then slot |
| def PkgSlotSort(row): |
| return (row[self._table.COL_PACKAGE], row[self._table.COL_SLOT]) |
| self._table.Sort(PkgSlotSort) |
| |
| if csv: |
| filehandle = self._OpenFileForWrite(csv) |
| if filehandle: |
| print "Writing package status as csv to %s" % csv |
| hiddencols = None |
| if not self._upgrade_deep and not self._upgrade: |
| hiddencols = set([self._table.COL_ACTION_TAKEN]) |
| self._table.WriteCSV(filehandle, hiddencols) |
| filehandle.close() |
| if html: |
| filehandle = self._OpenFileForWrite(html) |
| if filehandle: |
| print "Writing package status as html to %s" % html |
| self._table.WriteHTML(filehandle) |
| filehandle.close() |
| |
| def _CreateCommitMessage(self, upgrade_lines): |
| """Create appropriate git commit message for upgrades in |upgrade_lines|.""" |
| message = '' |
| upgrade_count = len(upgrade_lines) |
| upgrade_str = '\n'.join(upgrade_lines) |
| if upgrade_count == 1: |
| message = 'Upgrade the following Portage package\n\n%s\n' % upgrade_str |
| else: |
| message = ('Upgrade the following %d Portage packages\n\n%s\n' % |
| (upgrade_count, upgrade_str)) |
| |
| # The space before <fill-in> (at least for TEST=) fails pre-submit check, |
| # which is the intention here. |
| message += '\nBUG= <fill-in>' |
| message += '\nTEST= <fill-in>' |
| |
| return message |
| |
| def _AmendCommitMessage(self, upgrade_lines): |
| """Create git commit message combining |upgrade_lines| with last commit.""" |
| # First get the body of the last commit message. |
| git_cmd = 'show --pretty=format:"__BEGIN BODY__%n%b%n__END BODY__"' |
| result = self._RunGit(self._stable_repo, git_cmd, redirect_stdout=True) |
| match = re.search(r'__BEGIN BODY__\n(.+)__END BODY__', |
| result.output, re.DOTALL) |
| if match: |
| # Extract the upgrade_lines of last commit. |
| body = match.group(1) |
| for line in body.split('\n'): |
| if line: |
| upgrade_lines.append(line) |
| else: |
| break |
| |
| return self._CreateCommitMessage(upgrade_lines) |
| |
| def _UpgradePackages(self, infolist): |
| """Given a list of cpv info maps, adds the upstream cpv to the infos.""" |
| # An empty directory is needed to trick equery later. |
| self._emptydir = tempfile.mkdtemp() |
| |
| self._table.Clear() |
| dash_q = '' |
| if not self._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._upstream: |
| cros_lib.Info('Checking out cros/gentoo at %s as upstream reference.' % |
| self._upstream_repo) |
| self._RunGit(self._upstream_repo, 'checkout %s cros/gentoo' % dash_q) |
| upgrade_lines = [] |
| for info in infolist: |
| self._UpgradePackage(info) |
| if info['upgraded_cpv']: |
| upgrade_lines.append('Upgrade %s to %s' % |
| (info['cpv'], info['upgraded_cpv'])) |
| |
| # Give warnings for those that cannot be emerged after upgrade. |
| emerge_ok = True |
| pkg_keywords_needed = [] |
| for info in infolist: |
| if info['upgraded_cpv']: |
| if info['emerge_ok']: |
| cros_lib.Info('Confirmed that %s can be emerged after upgrade.' % |
| info['upgraded_cpv']) |
| if not info['emerge_stable']: |
| pkg_keywords_needed.append('=%s %s' % |
| (info['upgraded_cpv'], self._arch)) |
| else: |
| emerge_ok = False |
| cros_lib.Warning('Unable to emerge %s after upgrade.\n' |
| 'The emerge output follows:\n' % |
| info['upgraded_cpv']) |
| print info['emerge_output'] |
| |
| if not emerge_ok: |
| cros_lib.Die('Failed to complete upgrades (see above). Suggest' |
| ' adding additional packages to upgrade as needed.\n' |
| 'For now, you probably want to reset your changes:\n' |
| ' cd %s; git reset --hard; cd -' % |
| self._stable_repo) |
| # TODO(mtennant): On second thought, it's probably cleaner to reset the |
| # changes automatically. Will do this in another changelist. |
| elif upgrade_lines: |
| if self._amend: |
| message = self._AmendCommitMessage(upgrade_lines) |
| self._RunGit(self._stable_repo, "commit --amend -am '%s'" % message) |
| else: |
| message = self._CreateCommitMessage(upgrade_lines) |
| self._RunGit(self._stable_repo, "commit -am '%s'" % message) |
| |
| cros_lib.Info('Upgrade changes committed (see above),' |
| ' but message needs edit:\n' |
| ' cd %s; git commit --amend; cd -' % |
| self._stable_repo) |
| if pkg_keywords_needed: |
| cros_lib.Info('However, note that line(s) like the following' |
| ' must be added to\n %s:\n%s\n' |
| 'You should push this change first.' % |
| (self._GetPkgKeywordsFile(), |
| '\n'.join(pkg_keywords_needed))) |
| cros_lib.Info('If you wish to undo these changes instead:\n' |
| ' cd %s; git reset --hard HEAD^; cd -' % |
| self._stable_repo) |
| |
| finally: |
| if not self._upstream: |
| cros_lib.Info('Undoing checkout of cros/gentoo at %s.' % |
| self._upstream_repo) |
| self._RunGit(self._upstream_repo, 'checkout %s cros/master' % dash_q) |
| os.rmdir(self._emptydir) |
| |
| def _GenParallelEmergeArgv(self): |
| """Creates an argv for parallel_emerge based on current options.""" |
| argv = ['--emptytree', '--pretend'] |
| if self._board and self._board != self.HOST_BOARD: |
| argv.append('--board=%s' % self._board) |
| if not self._verbose: |
| argv.append('--quiet') |
| if self._rdeps: |
| argv.append('--root-deps=rdeps') |
| argv.extend(self._args) |
| |
| return argv |
| |
| def _SetPortTree(self, settings, trees): |
| """Set self._porttree from portage |settings| and |trees|.""" |
| root = settings["ROOT"] |
| self._porttree = trees[root]['porttree'] |
| |
| def _GetPortageDBAPI(self): |
| """Retrieve the Portage dbapi object, if available.""" |
| try: |
| return self._porttree.dbapi |
| except AttributeError: |
| return None |
| |
| 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 earlier in the list.""" |
| argv = self._GenParallelEmergeArgv() |
| |
| deps = parallel_emerge.DepGraphGenerator() |
| deps.Initialize(argv) |
| |
| deps_tree, deps_info = deps.GenDependencyTree() |
| |
| self._SetPortTree(deps.emerge.settings, deps.emerge.trees) |
| |
| self._deps_graph = deps.GenDependencyGraph(deps_tree, deps_info) |
| cpv_list = Upgrader._GetPreOrderDepGraph(self._deps_graph) |
| cpv_list.reverse() |
| return cpv_list |
| |
| 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 |
| |
| dbapi = self._GetPortageDBAPI() |
| ebuild_path = dbapi.findname2(cpv)[0] |
| (overlay, cat, pn, pv) = self._SplitEBuildPath(ebuild_path) |
| ver_rev = pv.replace(pn + '-', '') |
| slot, = dbapi.aux_get(cpv, ['SLOT']) |
| |
| infolist.append({'cpv': cpv, 'slot': slot, '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.""" |
| # Upfront check(s) if upgrade is requested. |
| if self._upgrade or self._upgrade_deep: |
| # Stable source must be on branch. |
| self._CheckStableRepoOnBranch() |
| |
| cpvlist = self._GetCurrentVersions() |
| infolist = self._GetInfoListWithOverlays(cpvlist) |
| self._UpgradePackages(infolist) |
| self._WriteTableFiles(csv=self._csv_file, |
| html=self._html_file) |
| |
| def _BoardIsSetUp(board): |
| """Return true if |board| has been setup.""" |
| return os.path.isdir('/build/%s' % board) |
| |
| def main(): |
| """Main function.""" |
| usage = 'Usage: %prog [options] packages...' |
| epilog = ('\n' |
| 'There are essentially two "modes": status report mode and ' |
| 'upgrade mode.\nStatus report mode is the default; upgrade ' |
| 'mode is enabled by either --upgrade or --upgrade-deep.\n' |
| '\n' |
| 'Status report mode will report on the status of the specified ' |
| 'packages relative to upstream,\nwithout making any changes.' |
| 'In this mode, the specified packages are often high-level\n' |
| 'targets such as "chromeos" or "chromeos-dev". ' |
| 'The --to-csv option is often used in this mode.\n' |
| 'The --stable-only option in this mode will make ' |
| 'the upstream comparison consider only stable versions.\n' |
| '\n' |
| 'Upgrade mode will attempt to upgrade the specified ' |
| 'packages to the latest upstream version.\nUnlike with --upgrade, ' |
| 'if --upgrade-deep is specified, then the package dependencies\n' |
| 'will also be upgraded.\n' |
| '\n' |
| 'Status report mode examples:\n' |
| '> cros_portage_upgrade --stable-only --board=tegra2_aebl ' |
| '--to-csv=cros-aebl.csv chromeos\n' |
| '> cros_portage_upgrade --board=x86-mario --to-csv=cros_test-mario ' |
| 'chromeos chromeos-dev chromeos-test\n' |
| 'Upgrade mode examples:\n' |
| '> cros_portage_upgrade --stable-only --board=tegra2_aebl ' |
| '--upgrade dbus\n' |
| '> cros_portage_upgrade --board=x86-mario --upgrade-deep gdata\n' |
| ) |
| |
| class MyOptParser(optparse.OptionParser): |
| """Override default epilog formatter, which strips newlines.""" |
| def format_epilog(self, formatter): |
| return self.epilog |
| |
| parser = MyOptParser(usage=usage, epilog=epilog) |
| parser.add_option('--amend', dest='amend', action='store_true', |
| default=False, |
| help="Amend existing commit when doing upgrade.") |
| 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('--stable-only', dest='stable_only', action='store_true', |
| default=False, |
| help="Use only stable upstream ebuilds for upgrades") |
| 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="Upgrade target package(s) only.") |
| parser.add_option('--upgrade-deep', dest='upgrade_deep', action='store_true', |
| default=False, |
| help="Upgrade target package(s) and all dependencies") |
| 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) |
| |
| 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') |
| |
| # The --upgrade and --upgrade-deep options are mutually exclusive. |
| if options.upgrade_deep and options.upgrade: |
| parser.print_help() |
| cros_lib.Die('The --upgrade and --upgrade-deep options ' + |
| 'are mutually exclusive.') |
| |
| # 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') |
| |
| # If a board is given, verify that the board is already set up. |
| if (options.board and options.board != Upgrader.HOST_BOARD and |
| not _BoardIsSetUp(options.board)): |
| parser.print_help() |
| cros_lib.Die('You must setup the %s board first.' % options.board) |
| |
| upgrader = Upgrader(options, args) |
| upgrader.Run() |
| |
| |
| if __name__ == '__main__': |
| main() |