#!/usr/bin/python -O import sys sys.path.insert(0, "/usr/lib/portage/pym") from copy import copy from optparse import OptionParser, OptionValueError import re import os, portage, portage_const class WorldHandler(object): def name(): return "world" name = staticmethod(name) def __init__(self): self.invalid = [] self.not_installed = [] self.okay = [] self.world_file = os.path.join("/", portage_const.WORLD_FILE) self.found = os.access(self.world_file, os.R_OK) for atom in open(self.world_file).read().split(): if not portage.isvalidatom(atom): self.invalid.append(atom) elif not portage.db["/"]["vartree"].dbapi.match(atom): self.not_installed.append(atom) else: self.okay.append(atom) def check(self): errors = [] if self.found: errors += map(lambda x: "'%s' is not a valid atom" % x, self.invalid) errors += map(lambda x: "'%s' is not installed" % x, self.not_installed) else: errors.append(self.world_file + " could not be opened for reading") return errors def fix(self): errors = [] try: portage.write_atomic(self.world_file, "\n".join(self.okay)) except OSError: errors.append(self.world_file + " could not be opened for writing") return errors class VdbKeyHandler(object): def name(): return "vdbkeys" name = staticmethod(name) def __init__(self): self.list = portage.db["/"]["vartree"].dbapi.cpv_all() self.missing = [] self.keys = ["HOMEPAGE", "SRC_URI", "KEYWORDS", "DESCRIPTION"] for p in self.list: mydir = os.path.join(os.sep, portage.settings["ROOT"], portage_const.VDB_PATH, p)+os.sep ismissing = True for k in self.keys: if os.path.exists(mydir+k): ismissing = False break if ismissing: self.missing.append(p) def check(self): return ["%s has missing keys" % x for x in self.missing] def fix(self): errors = [] for p in self.missing: mydir = os.path.join(os.sep, portage.settings["ROOT"], portage_const.VDB_PATH, p)+os.sep if not os.access(mydir+"environment.bz2", os.R_OK): errors.append("Can't access %s" % (mydir+"environment.bz2")) elif not os.access(mydir, os.W_OK): errors.append("Can't create files in %s" % mydir) else: env = os.popen("bzip2 -dcq "+mydir+"environment.bz2", "r") envlines = env.read().split("\n") env.close() for k in self.keys: s = [l for l in envlines if l.startswith(k+"=")] if len(s) > 1: errors.append("multiple matches for %s found in %senvironment.bz2" % (k, mydir)) elif len(s) == 0: s = "" else: s = s[0].split("=",1)[1] s = s.lstrip("$").strip("\'\"") s = re.sub("(\\\\[nrt])+", " ", s) s = " ".join(s.split()).strip() if s != "": try: keyfile = open(mydir+os.sep+k, "w") keyfile.write(s+"\n") keyfile.close() except (IOError, OSError), e: errors.append("Could not write %s, reason was: %s" % (mydir+k, e)) return errors def checkDict(dict): noneInstalled = [] noneAffected = [] # iterate over all entries in the dict for entry_cp in dict.keys(): # find installed packages of type entry iPkgs = portage.vardbapi(portage.root).match(entry_cp) # check if packages are installed if not len(iPkgs): noneInstalled.append(", ".join(dict[entry_cp])) continue count = 0 # now check for every installed package, if it is hit by the entry for i in iPkgs: count += len(portage.match_to_list(i, dict[entry_cp])) if count > 0: break; if count == 0: noneAffected.append(", ".join(dict[entry_cp])) continue #check if there are entries in the file, that didn't match #ie: package.keywords contains "=kde-base/kde-3.0.0 ... and =kde-base/kde-2.0.0 ..." # now if kde-4.0.0 is installed, kde-3.0.0 is still not recognized as redundant, as it # gives a match and count > 0. #solution: do the check again for single entries like "=*" and "~*" if len(dict[entry_cp]) > 1: for x in dict[entry_cp]: if x[0] == "=" or x[0] == "~": count = 0 # now check for every installed package, if it is hit by the entry for i in iPkgs: count += len(portage.match_to_list(i, [x])) if count > 0: break if count == 0: noneAffected.append(x) return (noneInstalled, noneAffected) def fixFile(fileName, noneInstalled, noneAffected): errors = [] strNoneInstalled = "NONE INSTALLED" strNoneAffected = "NONE AFFECTED" try: ifile = open(fileName) lines = ifile.readlines() ifile.close() ofile = open(fileName, "w") # now check every line if it contains a problematic entry for line in lines: prefix = "" for x in noneInstalled: if line.find(x) >= 0: prefix += strNoneInstalled for x in noneAffected: if line.find(x) >= 0: prefix += strNoneAffected if prefix: prefix = "# "+prefix+" " errors.append("fixed: " + prefix + line.strip()) #remove \n ofile.write(prefix+line) ofile.close() except OSError: errors.append(fileName + " could not be opened for reading or writing") return errors class PackageKeywordsHandler(object): def name(): return "p.keywords" name = staticmethod(name) def __init__(self): print "init" self.noneInstalled = [] self.noneAffected = [] # cache the config object cfg = portage.config(config_profile_path=portage.root, config_incrementals=portage_const.INCREMENTALS) #transforming the format of pkeywordsdict into something like punmaskdict i.e. key : [entry1, entry2, ...], key : [... dict = {} for k, v in cfg.pkeywordsdict.items(): t = [] for k2, v2 in v.items(): t.append(k2) dict[k] = t (self.noneInstalled , self.noneAffected) = checkDict( dict ) def check(self): errors = [] errors += map(lambda x: "'%s' has no installed packages" % x, self.noneInstalled) errors += map(lambda x: "'%s' affects no installed package(-version)" % x, self.noneAffected) return errors def fix(self): return fixFile("/etc/portage/package.keywords", self.noneInstalled, self.noneAffected) class PackageUnmaskHandler(object): def name(): return "p.unmask" name = staticmethod(name) def __init__(self): print "init" self.noneInstalled = [] self.noneAffected = [] # cache the config object cfg = portage.config(config_profile_path=portage.root, config_incrementals=portage_const.INCREMENTALS) (self.noneInstalled , self.noneAffected) = checkDict( cfg.punmaskdict ) def check(self): errors = [] errors += map(lambda x: "'%s' has no installed packages" % x, self.noneInstalled) errors += map(lambda x: "'%s' affects no installed package(-version)" % x, self.noneAffected) return errors def fix(self): return fixFile("/etc/portage/package.unmask", self.noneInstalled, self.noneAffected) # this sucks, should track this in a different manner. modules = {"world" : WorldHandler, "vdbkeys": VdbKeyHandler, "p.keywords" : PackageKeywordsHandler, "p.unmask" : PackageUnmaskHandler} #modules = {"world" : WorldHandler} module_names = modules.keys() module_names.sort() module_names.insert(0, "all") if __name__ == "__main__": def exclusive(option, *args, **kw): var = kw.get("var", None) if var is None: raise ValueError("var not specified to exclusive()") if getattr(parser, var, ""): raise OptionValueError("%s and %s are exclusive options" % (getattr(parser, var), option)) setattr(parser, var, str(option)) usage = "usage: emaint [options] " + " | ".join(module_names) usage+= "\n\nCurrently emaint can only check and fix problems with one's world\n" usage+= "file. Future versions will integrate other portage check-and-fix\n" usage+= "tools and provide a single interface to system health checks." parser = OptionParser(usage=usage) parser.add_option("-c", "--check", help="check for problems", action="callback", callback=exclusive, callback_kwargs={"var":"action"}) parser.add_option("-f", "--fix", help="attempt to fix problems", action="callback", callback=exclusive, callback_kwargs={"var":"action"}) parser.action = None (options, args) = parser.parse_args() if len(args) != 1: parser.error("Incorrect number of arguments") if args[0] not in module_names: parser.error("%s target is not a known target" % args[0]) if parser.action: action = parser.action else: print "Defaulting to --check" action = "-c/--check" if args[0] == "all": tasks = modules.values() else: tasks = [modules[args[0]]] if action == "-c/--check": status = "Checking %s for problems" func = "check" else: status = "Attempting to fix %s" func = "fix" for task in tasks: print status % task.name() inst = task() result = getattr(inst, func)() if result: print print "\n".join(result) print "\n" print "Finished"