diff options
Diffstat (limited to 'portato/gui/gui_helper.py')
-rw-r--r-- | portato/gui/gui_helper.py | 608 |
1 files changed, 608 insertions, 0 deletions
diff --git a/portato/gui/gui_helper.py b/portato/gui/gui_helper.py new file mode 100644 index 0000000..6cc09a3 --- /dev/null +++ b/portato/gui/gui_helper.py @@ -0,0 +1,608 @@ +# -*- coding: utf-8 -*- +# +# File: portato/gui/gui_helper.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2006 René 'Necoro' Neumann +# This is free software. You may redistribute copies of it under the terms of +# the GNU General Public License version 2. +# There is NO WARRANTY, to the extent permitted by law. +# +# Written by René 'Necoro' Neumann <necoro@necoro.net> + +# some backend things +from portato import backend +from portato.backend import flags +from portato.helper import * + +# parser +from portato.config_parser import ConfigParser + +# the wrapper +from wrapper import Console, Tree + +# some stuff needed +from subprocess import Popen, PIPE, STDOUT +from threading import Thread +import pty + +class Config: + """Wrapper around a ConfigParser and for additional local configurations.""" + const = { + "main_sec" : "Main", + "usePerVersion_opt" : "usePerVersion", + "useFile_opt" : "usefile", + "maskFile_opt" : "maskfile", + "maskPerVersion_opt" : "maskPerVersion", + "testingFile_opt" : "keywordfile", + "testingPerVersion_opt" : "keywordperversion", + "debug_opt" : "debug", + "oneshot_opt" : "oneshot", + "deep_opt" : "deep", + "newuse_opt" : "newuse", + "syncCmd_opt" : "synccommand" + } + + def __init__ (self, cfgFile): + """Constructor. + + @param cfgFile: path to config file + @type cfgFile: string""" + + # init ConfigParser + self._cfg = ConfigParser(cfgFile) + + # read config + self._cfg.parse() + + # local configs + self.local = {} + + def get(self, name, section=const["main_sec"], constName = True): + """Gets an option. + + @param name: name of the option + @type name: string + @param section: section to look in; default is Main-Section + @type section: string + @param constName: If True (the default), the option names are first looked up in the const-dict. + @type constName: boolean + @return: the option's value + @rtype: string""" + + if constName: + name = self.const[name] + + return self._cfg.get(name, section) + + def get_boolean(self, name, section=const["main_sec"], constName = True): + """Gets a boolean option. + + @param name: name of the option + @type name: string + @param section: section to look in; default is Main-Section + @type section: string + @param constName: If True (the default), the option names are first looked up in the const-dict. + @type constName: boolean + @return: the option's value + @rtype: boolean""" + + if constName: + name = self.const[name] + + return self._cfg.get_boolean(name, section) + + def modify_flags_config (self): + """Sets the internal config of the L{flags}-module. + @see: L{flags.set_config()}""" + + flagCfg = { + "usefile": self.get("useFile_opt"), + "usePerVersion" : self.get_boolean("usePerVersion_opt"), + "maskfile" : self.get("maskFile_opt"), + "maskPerVersion" : self.get_boolean("maskPerVersion_opt"), + "testingfile" : self.get("testingFile_opt"), + "testingPerVersion" : self.get_boolean("testingPerVersion_opt")} + flags.set_config(flagCfg) + + def modify_debug_config (self): + """Sets the external debug-config. + @see: L{helper.set_debug()}""" + set_debug(self.get_boolean("debug_opt")) + + def modify_external_configs (self): + """Convenience function setting all external configs.""" + self.modify_debug_config() + self.modify_flags_config() + + def set_local(self, cpv, name, val): + """Sets some local config. + + @param cpv: the cpv describing the package for which to set this option + @type cpv: string (cpv) + @param name: the option's name + @type name: string + @param val: the value to set + @type val: any""" + + if not cpv in self.local: + self.local[cpv] = {} + + self.local[cpv].update({name:val}) + + def get_local(self, cpv, name): + """Returns something out of the local config. + + @param cpv: the cpv describing the package from which to get this option + @type cpv: string (cpv) + @param name: the option's name + @type name: string + @return: value stored for the cpv and name or None if not found + @rtype: any""" + + if not cpv in self.local: + return None + if not name in self.local[cpv]: + return None + + return self.local[cpv][name] + + def set(self, name, val, section=const["main_sec"], constName = True): + """Sets an option. + + @param name: name of the option + @type name: string + @param val: value to set the option to + @type val: string + @param section: section to look in; default is Main-Section + @type section: string + @param constName: If True (the default), the option names are first looked up in the const-dict. + @type constName: boolean""" + + if constName: + name = self.const[name] + + self._cfg.set(name, val, section) + + def set_boolean (self, name, val, section=const["main_sec"], constName = True): + """Sets a boolean option. + + @param name: name of the option + @type name: string + @param val: value to set the option to + @type val: boolean + @param section: section to look in; default is Main-Section + @type section: string + @param constName: If True (the default), the option names are first looked up in the const-dict. + @type constName: boolean""" + + if constName: + name = self.const[name] + + self._cfg.set_boolean(name, val, section) + + def write(self): + """Writes to the config file and modify any external configs.""" + self._cfg.write() + self.modify_external_configs() + +class Database: + """An internal database which holds a simple dictionary cat -> [package_list].""" + + def __init__ (self): + """Constructor.""" + self._db = {} + + def populate (self, category = None): + """Populates the database. + + @param category: An optional category - so only packages of this category are inserted. + @type category: string""" + + # get the lists + packages = backend.find_all_packages(name = category, withVersion = False) + installed = backend.find_all_installed_packages(name = category, withVersion = False) + + # cycle through packages + for p in packages: + list = p.split("/") + cat = list[0] + pkg = list[1] + if p in installed: + pkg += "*" + if not cat in self._db: self._db[cat] = [] + self._db[cat].append(pkg) + + for key in self._db: # sort alphabetically + self._db[key].sort(cmp=cmp, key=str.lower) + + def get_cat (self, cat): + """Returns the packages in the category. + + @param cat: category to return the packages from + @type cat: string + @return: list of packages or [] + @rtype: string[]""" + + try: + return self._db[cat] + except KeyError: # cat is in category list - but not in portage + debug("Catched KeyError =>", cat, "seems not to be an available category. Have you played with rsync-excludes?") + return [] + + def reload (self, cat): + """Reloads the given category. + + @param cat: category + @type cat: string""" + + del self._db[cat] + self.populate(cat+"/") + +class EmergeQueue: + """This class manages the emerge queue.""" + + def __init__ (self, tree = None, console = None, db = None): + """Constructor. + + @param tree: Tree to append all the items to. + @type tree: Tree + @param console: Output is shown here. + @type console: Console + @param db: A database instance. + @type db: Database""" + + # the different queues + self.mergequeue = [] # for emerge + self.unmergequeue = [] # for emerge -C + self.oneshotmerge = [] # for emerge --oneshot + + # dictionaries with data about the packages in the queue + self.iters = {} # iterator in the tree + self.deps = {} # all the deps of the package + + # member vars + self.tree = tree + if self.tree and not isinstance(self.tree, Tree): raise TypeError, "tree passed is not a Tree-object" + + self.console = console + if self.console and not isinstance(self.console, Console): raise TypeError, "console passed is not a Console-object" + + self.db = db + + # our iterators pointing at the toplevels; they are set to None if we do not have a tree + if self.tree: + self.emergeIt = self.tree.get_emerge_it() + self.unmergeIt = self.tree.get_unmerge_it() + else: + self.emergeIt = self.unmergeIt = None + + def _get_pkg_from_cpv (self, cpv, unmask = False): + """Gets a L{backend.Package}-object from a cpv. + + @param cpv: the cpv to get the package for + @type cpv: string (cpv) + @param unmask: if True we will look for masked packages if we cannot find unmasked ones + @type unmask: boolean + @return: created package + @rtype: backend.Package + + @raises backend.PackageNotFoundException: If no package could be found - normally it is existing but masked.""" + + # for the beginning: let us create a package object - but it is not guaranteed, that it actually exists in portage + pkg = backend.Package(cpv) + masked = not (pkg.is_masked() or pkg.is_testing(allowed=True)) # we are setting this to True in case we have unmasked it already, but portage does not know this + + # and now try to find it in portage + pkg = backend.find_packages("="+cpv, masked = masked) + + if pkg: # gotcha + pkg = pkg[0] + + elif unmask: # no pkg returned, but we are allowed to unmask it + pkg = backend.find_packages("="+cpv, masked = True)[0] + if pkg.is_testing(allowed = True): + pkg.set_testing(True) + if pkg.is_masked(): + pkg.set_masked() + + else: # no pkg returned - and we are not allowed to unmask + raise backend.PackageNotFoundException(cpv) + + return pkg + + def update_tree (self, it, cpv, unmask = False, oneshot = False): + """This updates the tree recursivly, or? Isn't it? Bjorn! + + @param it: iterator where to append + @type it: Iterator + @param cpv: The package to append. + @type cpv: string (cat/pkg-ver) + @param unmask: True if we are allowed to look for masked packages + @type unmask: boolean + @param oneshot: True if we want to emerge is oneshot + @type oneshot: boolean + + @raises backend.BlockedException: When occured during dependency-calculation. + @raises backend.PackageNotFoundException: If no package could be found - normally it is existing but masked.""" + + if cpv in self.deps: + return # in list already and therefore it's already in the tree too + + update = False + uVersion = None + try: + pkg = self._get_pkg_from_cpv(cpv, unmask) + if not pkg.is_installed(): + old = backend.get_all_installed_versions(pkg.get_cp()) + if old: + old = old[0] # assume we have only one there; FIXME: slotted packages + update = True + uVersion = old.get_version() + + except backend.PackageNotFoundException, e: # package not found / package is masked -> delete current tree and re-raise the exception + if self.tree.iter_has_parent(it): + while self.tree.iter_has_parent(it): + it = self.tree.parent_iter(it) + self.remove_with_children(it) + raise e + + # add iter + subIt = self.tree.append(it, self.tree.build_append_value(cpv, oneshot = oneshot, update = update, version = uVersion)) + self.iters.update({cpv: subIt}) + + # get dependencies + deps = pkg.get_dep_packages() # this might raise a BlockedException + self.deps.update({cpv : deps}) + + # recursive call + for d in deps: + try: + self.update_tree(subIt, d, unmask) + except backend.BlockedException, e: # BlockedException occured -> delete current tree and re-raise exception + debug("Something blocked:", e[0]) + self.remove_with_children(subIt) + raise e + + def append (self, cpv, unmerge = False, update = False, forceUpdate = False, unmask = False, oneshot = False): + """Appends a cpv either to the merge queue or to the unmerge-queue. + Also updates the tree-view. + + @param cpv: Package to add + @type cpv: string (cat/pkg-ver) + @param unmerge: Set to True if you want to unmerge this package - else False. + @type unmerge: boolean + @param update: Set to True if a package is going to be updated (e.g. if the use-flags changed). + @type update: boolean + @param forceUpdate: Set to True if the update should be forced. + @type forceUpdate: boolean + @param unmask: True if we are allowed to look for masked packages + @type unmask: boolean + @param oneshot: True if this package should not be added to the world-file. + @type oneshot: boolean + + @raises portato.backend.PackageNotFoundException: if trying to add a package which does not exist""" + + if not unmerge: # emerge + # insert dependencies + pkg = self._get_pkg_from_cpv(cpv, unmask) + deps = pkg.get_dep_packages() + + if update: + if not forceUpdate and deps == self.deps[cpv]: + return # nothing changed - return + else: + hasBeenInQueue = (cpv in self.mergequeue or cpv in self.oneshotmerge) + parentIt = self.tree.parent_iter(self.iters[cpv]) + + # delete it out of the tree - but NOT the changed flags + self.remove_with_children(self.iters[cpv], removeNewFlags = False) + + if hasBeenInQueue: # package has been in queue before + self._queue_append(cpv, oneshot) + + self.update_tree(parentIt, cpv, unmask, oneshot = oneshot) + else: # not update + self._queue_append(cpv, oneshot) + if self.emergeIt: + self.update_tree(self.emergeIt, cpv, unmask, oneshot = oneshot) + + else: # unmerge + self.unmergequeue.append(cpv) + if self.unmergeIt: # update tree + self.tree.append(self.unmergeIt, self.tree.build_append_value(cpv)) + + def _queue_append (self, cpv, oneshot = False): + """Convenience function appending a cpv either to self.mergequeue or to self.oneshotmerge. + + @param cpv: cpv to add + @type cpv: string (cpv) + @param oneshot: True if this package should not be added to the world-file. + @type oneshot: boolean""" + + if not oneshot: + self.mergequeue.append(cpv) + else: + self.oneshotmerge.append(cpv) + + def _update_packages(self, packages, process = None): + """This updates the packages-list. It simply makes the db to rebuild the specific category. + + @param packages: The packages which we emerged. + @type packages: list of cpvs + @param process: The process we have to wait for before we can do our work. + @type process: subprocess.Popen""" + + if process: process.wait() + for p in packages: + if p in ["world", "system"]: continue + cat = backend.split_package_name(p)[0] # get category + while cat[0] in ["=",">","<","!"]: + cat = cat[1:] + self.db.reload(cat) + debug("Category %s refreshed" % cat) + + def _emerge (self, options, packages, it, command = ["/usr/bin/python","/usr/bin/emerge"]): + """Calls emerge and updates the terminal. + + @param options: options to send to emerge + @type options: list + @param packages: packages to emerge + @type packages: list + @param it: Iterators which point to these entries whose children will be removed after completion. + @type it: Iterator[] + @param command: the command to execute - default is "/usr/bin/python /usr/bin/emerge" + @type command: string[]""" + + # open tty + (master, slave) = pty.openpty() + self.console.set_pty(master) + + # start emerge + process = Popen(command+options+packages, stdout = slave, stderr = STDOUT, shell = False) + + # start thread waiting for the stop of emerge + Thread(target=self._update_packages, args=(packages+self.deps.keys(), process)).start() + + # remove + for i in it: + self.remove_with_children(i) + + def emerge (self, force = False): + """Emerges everything in the merge-queue. + + @param force: If False, '-pv' is send to emerge. Default: False. + @type force: boolean""" + + def prepare(queue): + """Prepares the list of iterators and the list of packages.""" + list = [] + its = [] + for k in queue: + list += ["="+k] + its.append(self.iters[k]) + + return list, its + + # oneshot-queue + if len(self.oneshotmerge) != 0: + # prepare package-list for oneshot + list, its = prepare(self.oneshotmerge) + + s = ["--oneshot"] + if not force: s += ["--verbose", "--pretend"] + + self._emerge(s, list, its) + + # normal queue + if len(self.mergequeue) != 0: + # prepare package-list + list, its = prepare(self.mergequeue) + + s = [] + if not force: s = ["--verbose", "--pretend"] + + self._emerge(s, list, its) + + def unmerge (self, force = False): + """Unmerges everything in the umerge-queue. + + @param force: If False, '-pv' is send to emerge. Default: False. + @type force: boolean""" + + if len(self.unmergequeue) == 0: return # nothing in queue + + list = self.unmergequeue[:] # copy the unmerge-queue + + # set options + s = ["-C"] + if not force: s += ["-pv"] + + self._emerge(s,list, [self.unmergeIt]) + + def update_world(self, force = False, newuse = False, deep = False): + """Does an update world. newuse and deep are the arguments handed to emerge. + + @param force: If False, '-pv' is send to emerge. Default: False. + @type force: boolean""" + + options = ["--update"] + + if newuse: options += ["--newuse"] + if deep: options += ["--deep"] + if not force: options += ["-pv"] + + self._emerge(options, ["world"], [self.emergeIt]) + + def sync (self, command = None): + """Calls "emerge --sync". + + @param command: command to execute to sync. If None "emerge --sync" is taken. + @type command: string[]""" + + if command == None: + self._emerge(["--sync"], [], []) + else: + self._emerge([],[],[], command = command) + + def remove_with_children (self, it, removeNewFlags = True): + """Convenience function which removes all children of an iterator and than the iterator itself. + + @param it: The iter which to remove. + @type it: Iterator + @param removeNewFlags: True if new flags should be removed; False otherwise. Default: True. + @type removeNewFlags: boolean""" + + self.remove_children(it, removeNewFlags) + self.remove(it, removeNewFlags) + + def remove_children (self, parentIt, removeNewFlags = True): + """Removes all children of a given parent TreeIter recursivly. + + @param parentIt: The iter from which to remove all children. + @type parentIt: Iterator + @param removeNewFlags: True if new flags should be removed; False otherwise. Default: True. + @type removeNewFlags: boolean""" + + childIt = self.tree.first_child_iter(parentIt) + + while childIt: + if (self.tree.iter_has_children(childIt)): # recursive call + self.remove_children(childIt, removeNewFlags) + temp = childIt + childIt = self.tree.next_iter(childIt) + self.remove(temp, removeNewFlags) + + def remove (self, it, removeNewFlags = True): + """Removes a specific item in the tree. This does not remove the top-entries. + + @param it: Iterator which points to the entry we are going to remove. + @type it: Iterator + @param removeNewFlags: True if new flags should be removed; False otherwise. Default: True. + @type removeNewFlags: boolean""" + + if self.tree.iter_has_parent(it): # NEVER remove our top stuff + cpv = self.tree.get_value(it, self.tree.get_cpv_column()) + if self.tree.is_in_emerge(it): # Emerge + del self.iters[cpv] + try: + del self.deps[cpv] + except KeyError: # this seems to be removed due to a BlockedException - so no deps here atm ;) + debug("Catched KeyError =>", cpv, "seems not to be in self.deps. Should be no harm in normal cases.") + try: + self.mergequeue.remove(cpv) + except ValueError: # this is a dependency - ignore + try: + self.oneshotmerge.remove(cpv) + except ValueError: + debug("Catched ValueError =>", cpv, "seems not to be in merge-queue. Should be no harm.") + + if removeNewFlags: # remove the changed flags + flags.remove_new_use_flags(cpv) + flags.remove_new_masked(cpv) + flags.remove_new_testing(cpv) + + else: # in Unmerge + self.unmergequeue.remove(cpv) + + self.tree.remove(it) |