blob: 085fe2a55fa48a231dd5f11231c03dd78fbe47be [file] [log] [blame]
# Copyright 2005-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
from _emerge.actions import load_emerge_config
import portage
from portage import os, _unicode_encode
from portage.const import MERGING_IDENTIFIER, PORTAGE_BIN_PATH, PRIVATE_PATH, \
VDB_PATH
from portage.dep import isvalidatom
import shutil
import subprocess
import sys
import time
class TrackingFile(object):
"""File for keeping track of failed merges."""
def __init__(self, tracking_path):
"""
Create a TrackingFile object.
@param tracking_path: file path used to keep track of failed merges
@type tracking_path: String
"""
self._tracking_path = _unicode_encode(tracking_path)
def save(self, failed_pkgs):
"""
Save the specified packages that failed to merge.
@param failed_pkgs: dictionary of failed packages
@type failed_pkgs: dict
"""
tracking_path = self._tracking_path
lines = ['%s %s' % (pkg, mtime) for pkg, mtime in failed_pkgs.items()]
portage.util.write_atomic(tracking_path, '\n'.join(lines))
def load(self):
"""
Load previously failed merges.
@rtype: dict
@return: dictionary of packages that failed to merge
"""
tracking_path = self._tracking_path
if not self.exists():
return {}
failed_pkgs = {}
with open(tracking_path, 'r') as tracking_file:
for failed_merge in tracking_file:
pkg, mtime = failed_merge.strip().split()
failed_pkgs[pkg] = mtime
return failed_pkgs
def exists(self):
"""
Check if tracking file exists.
@rtype: bool
@return: true if tracking file exists, false otherwise
"""
return os.path.exists(self._tracking_path)
def purge(self):
"""Delete previously saved tracking file if one exists."""
if self.exists():
os.remove(self._tracking_path)
def __iter__(self):
"""
Provide an interator over failed merges.
@return: iterator of packages that failed to merge
"""
return self.load().items().__iter__()
class MergesHandler(object):
"""Handle failed package merges."""
short_desc = "Remove failed merges"
@staticmethod
def name():
return "merges"
def __init__(self):
"""Create MergesHandler object."""
eroot = portage.settings['EROOT']
tracking_path = os.path.join(eroot, PRIVATE_PATH, 'failed-merges');
self._tracking_file = TrackingFile(tracking_path)
self._vardb_path = os.path.join(eroot, VDB_PATH)
def can_progressbar(self, func):
return func == 'check'
def _scan(self, onProgress=None):
"""
Scan the file system for failed merges and return any found.
@param onProgress: function to call for updating progress
@type onProgress: Function
@rtype: dict
@return: dictionary of packages that failed to merges
"""
failed_pkgs = {}
for cat in os.listdir(self._vardb_path):
pkgs_path = os.path.join(self._vardb_path, cat)
if not os.path.isdir(pkgs_path):
continue
pkgs = os.listdir(pkgs_path)
maxval = len(pkgs)
for i, pkg in enumerate(pkgs):
if onProgress:
onProgress(maxval, i+1)
if MERGING_IDENTIFIER in pkg:
mtime = int(os.stat(os.path.join(pkgs_path, pkg)).st_mtime)
pkg = os.path.join(cat, pkg)
failed_pkgs[pkg] = mtime
return failed_pkgs
def _failed_pkgs(self, onProgress=None):
"""
Return failed packages from both the file system and tracking file.
@rtype: dict
@return: dictionary of packages that failed to merges
"""
failed_pkgs = self._scan(onProgress)
for pkg, mtime in self._tracking_file:
if pkg not in failed_pkgs:
failed_pkgs[pkg] = mtime
return failed_pkgs
def _remove_failed_dirs(self, failed_pkgs):
"""
Remove the directories of packages that failed to merge.
@param failed_pkgs: failed packages whose directories to remove
@type failed_pkg: dict
"""
for failed_pkg in failed_pkgs:
pkg_path = os.path.join(self._vardb_path, failed_pkg)
# delete failed merge directory if it exists (it might not exist
# if loaded from tracking file)
if os.path.exists(pkg_path):
shutil.rmtree(pkg_path)
# TODO: try removing package CONTENTS to prevent orphaned
# files
def _get_pkg_atoms(self, failed_pkgs, pkg_atoms, pkg_invalid_entries):
"""
Get the package atoms for the specified failed packages.
@param failed_pkgs: failed packages to iterate
@type failed_pkgs: dict
@param pkg_atoms: append package atoms to this set
@type pkg_atoms: set
@param pkg_invalid_entries: append any packages that are invalid to this set
@type pkg_invalid_entries: set
"""
emerge_config = load_emerge_config()
portdb = emerge_config.target_config.trees['porttree'].dbapi
for failed_pkg in failed_pkgs:
# validate pkg name
pkg_name = '%s' % failed_pkg.replace(MERGING_IDENTIFIER, '')
pkg_atom = '=%s' % pkg_name
if not isvalidatom(pkg_atom):
pkg_invalid_entries.append("'%s' is an invalid package atom."
% pkg_atom)
if not portdb.cpv_exists(pkg_name):
pkg_invalid_entries.append(
"'%s' does not exist in the portage tree." % pkg_name)
pkg_atoms.add(pkg_atom)
def _emerge_pkg_atoms(self, module_output, pkg_atoms):
"""
Emerge the specified packages atoms.
@param module_output: output will be written to
@type module_output: Class
@param pkg_atoms: packages atoms to emerge
@type pkg_atoms: set
@rtype: list
@return: List of results
"""
# TODO: rewrite code to use portage's APIs instead of a subprocess
env = {
"FEATURES" : "-collision-protect -protect-owned",
"PATH" : os.environ["PATH"]
}
emerge_cmd = (
portage._python_interpreter,
'-b',
os.path.join(PORTAGE_BIN_PATH, 'emerge'),
'--quiet',
'--oneshot',
'--complete-graph=y'
)
results = []
msg = 'Re-Emerging packages that failed to merge...\n'
if module_output:
module_output.write(msg)
else:
module_output = subprocess.PIPE
results.append(msg)
proc = subprocess.Popen(emerge_cmd + tuple(pkg_atoms), env=env,
stdout=module_output, stderr=sys.stderr)
output = proc.communicate()[0]
if output:
results.append(output)
if proc.returncode != os.EX_OK:
emerge_status = "Failed to emerge '%s'" % (' '.join(pkg_atoms))
else:
emerge_status = "Successfully emerged '%s'" % (' '.join(pkg_atoms))
results.append(emerge_status)
return results
def check(self, **kwargs):
"""Check for failed merges."""
onProgress = kwargs.get('onProgress', None)
failed_pkgs = self._failed_pkgs(onProgress)
errors = []
for pkg, mtime in failed_pkgs.items():
mtime_str = time.ctime(int(mtime))
errors.append("'%s' failed to merge on '%s'" % (pkg, mtime_str))
return errors
def fix(self, **kwargs):
"""Attempt to fix any failed merges."""
module_output = kwargs.get('module_output', None)
failed_pkgs = self._failed_pkgs()
if not failed_pkgs:
return ['No failed merges found.']
pkg_invalid_entries = set()
pkg_atoms = set()
self._get_pkg_atoms(failed_pkgs, pkg_atoms, pkg_invalid_entries)
if pkg_invalid_entries:
return pkg_invalid_entries
try:
self._tracking_file.save(failed_pkgs)
except IOError as ex:
errors = ['Unable to save failed merges to tracking file: %s\n'
% str(ex)]
errors.append(', '.join(sorted(failed_pkgs)))
return errors
self._remove_failed_dirs(failed_pkgs)
results = self._emerge_pkg_atoms(module_output, pkg_atoms)
# list any new failed merges
for pkg in sorted(self._scan()):
results.append("'%s' still found as a failed merge." % pkg)
# reload config and remove successful packages from tracking file
emerge_config = load_emerge_config()
vardb = emerge_config.target_config.trees['vartree'].dbapi
still_failed_pkgs = {}
for pkg, mtime in failed_pkgs.items():
pkg_name = '%s' % pkg.replace(MERGING_IDENTIFIER, '')
if not vardb.cpv_exists(pkg_name):
still_failed_pkgs[pkg] = mtime
self._tracking_file.save(still_failed_pkgs)
return results
def purge(self, **kwargs):
"""Attempt to remove previously saved tracking file."""
if not self._tracking_file.exists():
return ['Tracking file not found.']
self._tracking_file.purge()
return ['Removed tracking file.']