diff --git a/libchannels/apt_cache.py b/libchannels/apt_cache.py new file mode 100644 index 0000000..853c7b2 --- /dev/null +++ b/libchannels/apt_cache.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# libchannels - update channels management library +# Copyright (C) 2015 Eugenio "g7" Paolantonio +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +import apt +import apt_pkg + +import logging + +logger = logging.getLogger(__name__) + +class DynamicCache: + """ + A proxy object that will load the cache only when required. + """ + + def __init__(self): + """ + Initializes the object. + """ + + self._cache = None + + def open_cache(self, progress=None): + """ + Opens/creates the cache. + """ + + if not self._cache: + self._cache = apt.Cache(progress=progress) + else: + self._cache.open(progress=progress) + + def clear(self): + """ + Clears the changes made. + """ + + if self._cache: + self._cache = None + + def __getattr__(self, name): + """ + Returns the cache attribute, loading the cache first if required. + """ + + if not self._cache: + self.open_cache() + + logger.debug("Cache attribute requested, %s" % name) + return getattr(self._cache, name) + + def __getitem__(self, name): + """ + Offloads the call to the cache object. + """ + + if not self._cache: + self.open_cache() + + return self._cache.__getitem__(name) + + def __contains__(self, name): + """ + Offloads the call to the cache object. + """ + + if not self._cache: + self.open_cache() + + return self._cache.__contains__(name) + + def __iter__(self): + """ + Offloads the iteration to the cache object. + """ + + if not self._cache: + self.open_cache() + + return self._cache.__iter__() + + def __eq__(self, other): + """ + Offloads the comparison to the cache object. + """ + + return self._cache.__eq__(other) + + def __ne__(self, other): + """ + Offloads the comparison to the cache object. + """ + + return self._cache.__ne__(other) diff --git a/libchannels/updates.py b/libchannels/apt_handler.py similarity index 83% rename from libchannels/updates.py rename to libchannels/apt_handler.py index b94dc7c..48e793c 100644 --- a/libchannels/updates.py +++ b/libchannels/apt_handler.py @@ -23,13 +23,17 @@ import logging -import libchannels.common - from apt_pkg import size_to_str logger = logging.getLogger(__name__) -class Updates: +# APT Configuration + +# FIXME: should make this interactive +apt_pkg.config.set("DPkg::Options::", "--force-confdef") +apt_pkg.config.set("DPkg::Options::", "--force-confold") + +class APT: """ Same-release distribution updates management. @@ -37,7 +41,7 @@ class Updates: MAX_TRIES = 5 - def __init__(self): + def __init__(self, cache): """ Initializes the class. """ @@ -53,8 +57,8 @@ def __init__(self): self.lock_failure_callback = None self.generic_failure_callback = None - - self.cache = None + + self.cache = cache self.cache_progress = None self.cache_acquire_progress = None @@ -85,10 +89,7 @@ def open_cache(self, progress=None): Opens/Creates the cache. """ - if not self.cache: - self.cache = apt.Cache(progress=self.cache_progress) - else: - self.cache.open(progress=self.cache_progress) + return self.cache.open_cache(progress if progress else self.cache_progress) def clear(self): """ @@ -96,7 +97,7 @@ def clear(self): """ if self.cache: - self.cache = None + self.cache.clear() self.changed = False @@ -111,8 +112,55 @@ def get_update_infos(self): if not self.cache: return 0, 0 - return self.cache.required_download, self.cache.required_space - + try: + return self.cache.required_download, self.cache.required_space + except Exception as err: + self.notify_error("An error occurred", err) + return 0, 0 + + def check_relationship(self, relationship): + """ + Checks a single relationship. + + Returns True if it does, False if not. + + `relationship` is a single relationship. + """ + + for package_name, relationship_version, operator in relationship: + if package_name in self.cache: + package = self.cache[package_name] + if package.is_installed: + version = package.installed + elif package.marked_install or package.marked_upgrade or package.marked_downgrade: + # FIXME: this assumption may not be always correct + version = package.candidate + + if apt_pkg.check_dep(version.version, operator, relationship_version): + return True + + return False + + def check_relationships(self, relationships): + """ + Checks if the current APT state satisfies the given relationships. + + Returns True if it does, False if not. + + `relationships` is a relationship list as returned by `apt_pkg.parse_src_depends()`. + """ + + # Example relationship list: + # [[('abiword', '5.0', '>='), ('meh', '', '')], [('gnumeric', '', ''), ('lol', '', '')]] + # which roughly translates to: + # abiword (>= 5.0) | meh, gnumeric | lol + + for group in relationships: + if not self.check_relationship(group): + return False + + return True + def update(self): """ Updates the package cache. @@ -203,6 +251,8 @@ def install(self): if not self.fetch(package_manager): return + logger.debug("Starting the update run") + # Then run the actual installation process res = self.packages_install_progress.run(package_manager) #res = package_manager.do_install() @@ -221,6 +271,7 @@ def install(self): ) # Dpkg journal dirty? if self.cache.dpkg_journal_dirty: + logger.debug("Dirty journal, fixing...") subprocess.call(["dpkg", "configure", "--all"]) # FIXME: add check for core packages @@ -228,11 +279,15 @@ def install(self): # Broken packages? #if self.cache._depcache.broken_count > 0: if True: + logger.debug("Broken packages, fixing...") + self.cache.clear() + self.open_cache() + package_manager = apt_pkg.PackageManager(self.cache._depcache) fixer = apt_pkg.ProblemResolver(self.cache._depcache) fixer.resolve(True) # FIXME: should notify this? - self.fetch(package_manager) + #self.fetch(package_manager) self.packages_install_progress.run(package_manager) # Restore working state for the next upgrade run diff --git a/libchannels/common.py b/libchannels/common.py index 2302bcf..2c38269 100644 --- a/libchannels/common.py +++ b/libchannels/common.py @@ -18,16 +18,27 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # +import os + import logging import apt_pkg import aptsources.sourceslist +import libchannels.apt_handler +import libchannels.apt_cache + logger = logging.getLogger(__name__) # Sourceslist sourceslist = aptsources.sourceslist.SourcesList() +# APT cache +cache_object = libchannels.apt_cache.DynamicCache() + +# APT handler +handler_object = libchannels.apt_handler.APT(cache_object) + def lock( lock_failed_callback=None ):