diff --git a/pisi/cli/listorphaned.py b/pisi/cli/listorphaned.py new file mode 100644 index 00000000..d52b313d --- /dev/null +++ b/pisi/cli/listorphaned.py @@ -0,0 +1,59 @@ +# -*- coding:utf-8 -*- +# +# Copyright (C) 2014, marcin.bojara (at) gmail.com +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# Please read the COPYING file. +# + +import gettext +import optparse + +import pisi.cli.command as command +import pisi.context as ctx +import pisi.db +import pisi.util as util + +__trans = gettext.translation('pisi', fallback=True) +_ = __trans.gettext + + +class ListOrphaned(command.Command, metaclass=command.autocommand): + __doc__ = _("""List orphaned packages + +Usage: list-orphaned + +Lists packages installed as dependency, but no longer needed by any other installed package. +""") + + def __init__(self, args): + super(ListOrphaned, self).__init__(args) + self.installdb = pisi.db.installdb.InstallDB() + + name = ("list-orphaned", "lo") + + def options(self): + + group = optparse.OptionGroup(self.parser, _("list-orphaned options")) + group.add_option("-a", "--all", action="store_true", + default=False, help=_("Show all packages without reverse dependencies")) + group.add_option("-x", "--exclude", action="append", + default=None, help=_("Ignore packages and components whose basenames match pattern.")) + self.parser.add_option_group(group) + + def run(self): + + self.init(database = True, write = False) + orphaned = self.installdb.get_no_rev_deps() if self.options.all else self.installdb.get_orphaned() + + if self.options.exclude: + orphaned = pisi.blacklist.exclude(orphaned, ctx.get_option('exclude')) + + if orphaned: + ctx.ui.info(_("Orphaned packages:")) + ctx.ui.info(util.format_by_columns(sorted(orphaned))) + else: ctx.ui.info(_("No orphaned packages")) \ No newline at end of file diff --git a/pisi/cli/pisicli.py b/pisi/cli/pisicli.py index 983e5395..048c1e66 100644 --- a/pisi/cli/pisicli.py +++ b/pisi/cli/pisicli.py @@ -10,55 +10,59 @@ # Please read the COPYING file. # -import sys -import optparse - import gettext -__trans = gettext.translation('pisi', fallback=True) -_ = __trans.gettext +import optparse +import sys import pisi import pisi.cli -import pisi.cli.command as command import pisi.cli.addrepo import pisi.cli.blame import pisi.cli.build import pisi.cli.check import pisi.cli.clean +import pisi.cli.command as command import pisi.cli.configurepending import pisi.cli.deletecache import pisi.cli.delta +import pisi.cli.disablerepo import pisi.cli.emerge +import pisi.cli.enablerepo import pisi.cli.fetch import pisi.cli.graph +import pisi.cli.history import pisi.cli.index import pisi.cli.info import pisi.cli.install -import pisi.cli.history -import pisi.cli.listnewest import pisi.cli.listavailable import pisi.cli.listcomponents import pisi.cli.listinstalled +import pisi.cli.listnewest +import pisi.cli.listorphaned import pisi.cli.listpending import pisi.cli.listrepo import pisi.cli.listsources import pisi.cli.listupgrades import pisi.cli.rebuilddb import pisi.cli.remove +import pisi.cli.removeorphaned import pisi.cli.removerepo -import pisi.cli.enablerepo -import pisi.cli.disablerepo -import pisi.cli.searchfile import pisi.cli.search +import pisi.cli.searchfile import pisi.cli.updaterepo import pisi.cli.upgrade -#FIXME: why does this has to be imported last +# FIXME: why does this has to be imported last import pisi.cli.help +__trans = gettext.translation('pisi', fallback=True) +_ = __trans.gettext + + class ParserError(pisi.Exception): pass + class PreParser(optparse.OptionParser): """consumes any options, and finds arguments from command line""" @@ -68,11 +72,11 @@ class PreParser(optparse.OptionParser): def error(self, msg): raise ParserError(msg) - def parse_args(self, args=None): + def parse_args(self, args=None, values=None): self.opts = [] self.rargs = self._get_args(args) self._process_args() - return (self.opts, self.args) + return self.opts, self.args def _process_args(self): args = [] @@ -81,6 +85,7 @@ class PreParser(optparse.OptionParser): first_arg = False while rargs: arg = rargs[0] + def option(): if not self.allow_interspersed_args and first_arg: self.error(_('Options must precede non-option arguments')) @@ -118,7 +123,7 @@ class PisiCLI(object): self.parser = PreParser(version="%prog " + pisi.__version__) try: opts, args = self.parser.parse_args(args=orig_args) - if len(args)==0: # more explicit than using IndexError + if len(args) == 0: # more explicit than using IndexError if 'version' in opts: self.parser.print_version() sys.exit(0) diff --git a/pisi/cli/removeorphaned.py b/pisi/cli/removeorphaned.py new file mode 100644 index 00000000..039695a7 --- /dev/null +++ b/pisi/cli/removeorphaned.py @@ -0,0 +1,55 @@ +# -*- coding:utf-8 -*- +# +# Copyright (C) 2014, marcin.bojara (at) gmail.com +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# Please read the COPYING file. +# + +import gettext +import optparse + +import pisi.api +import pisi.cli.command as command +import pisi.context as ctx +import pisi.db + +__trans = gettext.translation('pisi', fallback=True) +_ = __trans.gettext + + +class RemoveOrphaned(command.PackageOp, metaclass=command.autocommand): + __doc__ = _("""Remove orphaned packages + +Usage: remove-orphaned + +Remove all orphaned packages from the system. +""") + + def __init__(self,args): + super(RemoveOrphaned, self).__init__(args) + self.installdb = pisi.db.installdb.InstallDB() + + name = ("remove-orphaned", "ro") + + def options(self): + group = optparse.OptionGroup(self.parser, _("remove-orphaned options")) + + super(RemoveOrphaned, self).options(group) + group.add_option("-x", "--exclude", action="append", + default=None, help=_("When removing orphaned, ignore packages and components whose basenames match pattern.")) + + self.parser.add_option_group(group) + + def run(self): + + self.init(database = True, write = False) + orphaned = self.installdb.get_orphaned() + if ctx.get_option('exclude'): + orphaned = pisi.blacklist.exclude(orphaned, ctx.get_option('exclude')) + + pisi.api.remove(orphaned) diff --git a/pisi/comariface.py b/pisi/comariface.py index 66615b49..8dcf5521 100644 --- a/pisi/comariface.py +++ b/pisi/comariface.py @@ -36,8 +36,9 @@ def is_char_valid(char): def is_method_missing(exception): """Tells if exception is about missing method in COMAR script""" - if exception._dbus_error_name in ("tr.org.pardus.comar.python.missing", - "tr.org.pardus.comar.Missing"): + if exception.get_dbus_name() in ("tr.org.pardus.comar.python.missing", "tr.org.pardus.comar.Missing"): + return True + if exception.__context__.get_dbus_name() in ("tr.org.pardus.comar.python.missing", "tr.org.pardus.comar.Missing"): return True return False diff --git a/pisi/constants.py b/pisi/constants.py index eacc325a..89e2a226 100644 --- a/pisi/constants.py +++ b/pisi/constants.py @@ -95,6 +95,7 @@ class Constants(metaclass=Singleton): self.__c.needs_reboot = "needsreboot" self.__c.files_db = "files.db" self.__c.repos = "repos" + self.__c.installed_extra = "installedextra" #file/directory permissions self.__c.umask = 0o022 diff --git a/pisi/db/installdb.py b/pisi/db/installdb.py index 04aee990..b4d53002 100644 --- a/pisi/db/installdb.py +++ b/pisi/db/installdb.py @@ -67,6 +67,15 @@ class InstallDB(lazydb.LazyDB): def init(self): self.installed_db = self.__generate_installed_pkgs() self.rev_deps_db = self.__generate_revdeps() + self.installed_extra = self.__generate_installed_extra() + + def __generate_installed_extra(self): + ie = [] + ie_path = os.path.join(ctx.config.info_dir(), ctx.const.installed_extra) + if os.path.isfile(ie_path): + with open(ie_path) as ie_file: + ie.extend(ie_file.read().strip().split("\n")) + return ie def __generate_installed_pkgs(self): def split_name(dirname): @@ -249,6 +258,19 @@ class InstallDB(lazydb.LazyDB): return rev_deps + def get_orphaned(self): + """ + get list of packages installed as extra dependency, + but without reverse dependencies now. + """ + return [x for x in self.installed_extra if not self.get_rev_deps(x)] + + def get_no_rev_deps(self): + """ + get installed packages list which haven't reverse dependencies. + """ + return [x for x in self.installed_db if not self.get_rev_deps(x)] + def pkg_dir(self, pkg, version, release): return pisi.util.join_path(ctx.config.packages_dir(), pkg + '-' + version + '-' + release) diff --git a/pisi/db/lazydb.py b/pisi/db/lazydb.py index dd0c4104..f66039fc 100644 --- a/pisi/db/lazydb.py +++ b/pisi/db/lazydb.py @@ -20,10 +20,12 @@ import string # lower borks for international locales. What we want is ascii lower. lower_map = str.maketrans(string.ascii_uppercase, string.ascii_lowercase) + class Singleton(object): _the_instances = {} + def __new__(type): - if not type.__name__ in Singleton._the_instances: + if type.__name__ not in Singleton._the_instances: Singleton._the_instances[type.__name__] = object.__new__(type) return Singleton._the_instances[type.__name__] diff --git a/pisi/operations/install.py b/pisi/operations/install.py index 55400f91..74db98c9 100644 --- a/pisi/operations/install.py +++ b/pisi/operations/install.py @@ -27,7 +27,8 @@ import pisi.pgraph as pgraph import pisi.ui as ui import pisi.db -def install_pkg_names(A, reinstall = False): + +def install_pkg_names(A, reinstall=False, extra=False): """This is the real thing. It installs packages from the repository, trying to perform a minimum number of installs""" @@ -78,7 +79,8 @@ def install_pkg_names(A, reinstall = False): if ctx.get_option('dry_run'): return True - if set(order) - A_0: + extra_packages = set(order) - A_0 + if extra_packages: if not ctx.ui.confirm(_('There are extra packages due to dependencies. Do you want to continue?')): return False @@ -91,10 +93,18 @@ def install_pkg_names(A, reinstall = False): conflicts = operations.helper.check_conflicts(order, packagedb) paths = [] + extra_paths = {} for x in order: ctx.ui.info(util.colorize(_("Downloading %d / %d") % (order.index(x)+1, len(order)), "yellow")) install_op = atomicoperations.Install.from_name(x) paths.append(install_op.package_fname) + if x in extra_packages or (extra and x in A): + extra_paths[install_op.package_fname] = x + elif reinstall and x in installdb.installed_extra: + installdb.installed_extra.remove(x) + with open(os.path.join(ctx.config.info_dir(), ctx.const.installed_extra), "w") as ie_file: + ie_file.write("\n".join(installdb.installed_extra) + ("\n" if installdb.installed_extra else "")) + # fetch to be installed packages but do not install them. if ctx.get_option('fetch_only'): @@ -107,6 +117,12 @@ def install_pkg_names(A, reinstall = False): ctx.ui.info(util.colorize(_("Installing %d / %d") % (paths.index(path)+1, len(paths)), "yellow")) install_op = atomicoperations.Install(path) install_op.install(False) + try: + with open(os.path.join(ctx.config.info_dir(), ctx.const.installed_extra), "a") as ie_file: + ie_file.write("%s\n" % extra_paths[path]) + installdb.installed_extra.append(extra_paths[path]) + except KeyError: + pass return True @@ -199,7 +215,7 @@ def install_pkg_files(package_URIs, reinstall = False): ctx.ui.info(util.format_by_columns(sorted(extra_packages))) if not ctx.ui.confirm(_('Do you want to continue?')): raise pisi.Error(_('External dependencies not satisfied')) - install_pkg_names(extra_packages, reinstall=True) + install_pkg_names(extra_packages, reinstall=True, extra=True) class PackageDB: def get_package(self, key, repo = None): diff --git a/pisi/operations/remove.py b/pisi/operations/remove.py index 11692d11..c1d69789 100644 --- a/pisi/operations/remove.py +++ b/pisi/operations/remove.py @@ -10,19 +10,20 @@ # Please read the COPYING file. # +import gettext import sys -import gettext +import pisi +import pisi.atomicoperations as atomicoperations +import pisi.context as ctx +import pisi.db +import pisi.pgraph as pgraph +import pisi.ui as ui +import pisi.util as util + __trans = gettext.translation('pisi', fallback=True) _ = __trans.gettext -import pisi -import pisi.context as ctx -import pisi.atomicoperations as atomicoperations -import pisi.pgraph as pgraph -import pisi.util as util -import pisi.ui as ui -import pisi.db def remove(A, ignore_dep = False, ignore_safety = False): """remove set A of packages from system (A is a list of package names)""" @@ -81,6 +82,10 @@ in the respective order to satisfy dependencies: for x in order: if installdb.has_package(x): atomicoperations.remove_single(x) + if x in installdb.installed_extra: + installdb.installed_extra.remove(x) + with open(os.path.join(ctx.config.info_dir(), ctx.const.installed_extra), "w") as ie_file: + ie_file.write("\n".join(installdb.installed_extra) + ("\n" if installdb.installed_extra else "")) else: ctx.ui.info(_('Package %s is not installed. Cannot remove.') % x)