I think my work on adding support for skipping packages causing depsolve problems in yumbase is ready for inclusion in yum git master.
the changes in the depsolve is minor, nothing there should break anything.

The next step after the code is included is to add a command line option to enable it in yum-cli.
should we stick with ''--skip-broken" or is there any better ideas.

Let me know what you think

Tim

diff --git a/test/skipbroken-tests.py b/test/skipbroken-tests.py
new file mode 100644
index 0000000..0c200dd
--- /dev/null
+++ b/test/skipbroken-tests.py
@@ -0,0 +1,271 @@
+import unittest
+import logging
+import sys
+from testbase import *
+
+class SkipBrokenTests(DepsolveTests):
+    ''' Test cases to test skip-broken'''
+    
+    def setup_func(self):
+        setup_logging()
+
+    def testMissingReqNoSkip(self):
+        ''' install fails,  because of missing req.
+        bar fails because foobar is not provided '''
+        po = FakePackage('foo', '1', '0', '0', 'noarch')
+        po.addRequires('bar', None, (None,None,None))
+        self.tsInfo.addInstall(po)
+
+        xpo = FakePackage('bar', '1', '0', '0', 'noarch')
+        xpo.addRequires('foobar', None, (None,None,None))
+        self.xsack.addPackage(xpo)
+        self.assertEquals('err', *self.resolveCode(skip=False))
+        self.assertResult((po,xpo))
+
+    def testMissingReqSkip(self):
+        ''' install is skipped, because of missing req.
+        foo + bar is skipped, because foobar is not provided '''
+        po = FakePackage('foo', '1', '0', '0', 'noarch')
+        po.addRequires('bar', None, (None,None,None))
+        self.tsInfo.addInstall(po)
+
+        xpo = FakePackage('bar', '1', '0', '0', 'noarch')
+        xpo.addRequires('foobar', None, (None,None,None))
+        self.xsack.addPackage(xpo)
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([])
+
+    def testDepWithMissingReqSkip(self):
+        ''' install is skipped, beacuse dep is missing req.  
+        foo + foobar is skipped because barfoo is not provided
+        bar stays in the transaction
+        '''
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po1.addRequires('foobar', None, (None,None,None))
+        self.tsInfo.addInstall(po1)
+
+        po2 = FakePackage('bar', '1', '0', '0', 'noarch')
+        self.tsInfo.addInstall(po2)
+
+        xpo1 = FakePackage('foobar', '1', '0', '0', 'noarch')
+        xpo1.addRequires('barfoo', None, (None,None,None))
+        self.xsack.addPackage(xpo1)
+        self.assertEquals('ok', *self.resolveCode(skip=True))
+        self.assertResult([po2])
+
+    def testUpdateOldRequired(self):
+        ''' update breaking req. of installed package is skipped
+        foo-1.0 -> foo-2.0 breaks the installed foo-tools needing foo-1.0
+        so skip the update and we have and empty transaction 
+        '''
+        # FIXME: The right solution is to skip the update from the transaction 
+        
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po2 = FakePackage('foo', '2', '0', '0', 'noarch')
+
+        ipo = FakePackage('foo-tools', '2.5', '0', '0', 'noarch')
+        ipo.addRequires('foo', 'EQ', ('0', '1', '0'))
+
+        self.rpmdb.addPackage(po1)
+        self.tsInfo.addUpdate(po2, oldpo=po1)
+        self.rpmdb.addPackage(ipo)
+        
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([ipo, po1])
+
+    def testUpdateRequireOld(self):
+        '''update with missing req. is skipped
+        The foo-1.0 -> foo-2.0 update fails, because foo-tools-2.0 need by foo-2.0
+        is not provided, the update should be skipped and result in a empty transaction
+        '''
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po1.addRequires('foo-tools', 'EQ', ('0', '1', '0'))
+        po2 = FakePackage('foo', '2', '0', '0', 'noarch')
+        po2.addRequires('foo-tools', 'EQ', ('0', '2', '0'))
+
+        ipo = FakePackage('foo-tools', '1', '0', '0', 'noarch')
+
+        self.rpmdb.addPackage(po1)
+        self.tsInfo.addUpdate(po2, oldpo=po1)
+        self.rpmdb.addPackage(ipo)
+        
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([ipo, po1])
+
+    def testUpdateRequireBoth(self):
+        ''' install + update skipped, because of missing req.
+        foo-1.0 -> foo-2.0 update, fails because foo-tools-2.0, needed by foo-2.0 is not provided.
+        foo-2.0 update get skip, and the foo-gui install will get skipped too, because it need foo-2.0
+        there is not longer provided.
+        '''
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po1.addRequires('foo-tools', 'EQ', ('0', '1', '0'))
+        po2 = FakePackage('foo', '2', '0', '0', 'noarch')
+        po2.addRequires('foo-tools', 'EQ', ('0', '2', '0'))
+
+        ipo = FakePackage('foo-tools', '1', '0', '0', 'noarch')
+        por =  FakePackage('foo-gui', '1', '0', '0', 'noarch')
+        por.addRequires('foo', 'EQ', ('0', '2', '0'))
+
+        self.rpmdb.addPackage(po1)
+        self.tsInfo.addUpdate(po2, oldpo=po1)
+        self.rpmdb.addPackage(ipo)
+        self.tsInfo.addInstall(por)
+
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([ipo, po1])
+
+    def testEraseDep(self):
+        ''' remove a package that someone depends on
+        foo is removed, and foo-tools get removed too, because it 
+        depends on foo  
+        '''
+        ipo = FakePackage('foo', '1', '0', '0', 'noarch')
+        ipo2 = FakePackage('foo-tools', '1', '0', '0', 'noarch')
+        ipo2.addRequires('foo', 'EQ', ('0', '1', '0'))
+
+        self.rpmdb.addPackage(ipo)
+        self.rpmdb.addPackage(ipo2)
+
+        self.tsInfo.addErase(ipo)
+        self.assertEquals('ok', *self.resolveCode(skip=True))
+        self.assertResult([])
+
+    def testEraseReqByUpdateNoSkip(self):
+        ''' update fails, because a req is erased.
+        Update foo-tools-1.0 -> foo-tools-2.0, should fail because the require foo is removed
+        '''
+        ipo = FakePackage('foo', '1', '0', '0', 'noarch')
+        ipo2 = FakePackage('foo-tools', '1', '0', '0', 'noarch')
+        ipo2.addRequires('foo', 'EQ', ('0', '1', '0'))
+
+        upo2 = FakePackage('foo-tools', '2', '0', '0', 'noarch')
+        upo2.addRequires('foo', 'EQ', ('0', '1', '0'))
+
+        self.rpmdb.addPackage(ipo)
+        self.rpmdb.addPackage(ipo2)   
+        self.tsInfo.addErase(ipo)
+        self.tsInfo.addUpdate(upo2, oldpo=ipo2)
+        
+        self.assertEquals('err', *self.resolveCode(skip=False))
+
+    def testEraseReqByUpdateSkip(self):
+        ''' update is skipped, because a req is erased.
+        Update foo-tools-1.0 -> foo-tools-2.0, should fail because the require foo is removed
+        the update is skipped and foo-tools-1.0 is removed too, because it requires foo. 
+        '''
+        ipo = FakePackage('foo', '1', '0', '0', 'noarch')
+        ipo2 = FakePackage('foo-tools', '1', '0', '0', 'noarch')
+        ipo2.addRequires('foo', 'EQ', ('0', '1', '0'))
+
+        upo2 = FakePackage('foo-tools', '2', '0', '0', 'noarch')
+        upo2.addRequires('foo', 'EQ', ('0', '1', '0'))
+
+        self.rpmdb.addPackage(ipo)
+        self.rpmdb.addPackage(ipo2)   
+        self.tsInfo.addUpdate(upo2, oldpo=ipo2)
+        self.tsInfo.addErase(ipo)
+        
+        self.assertEquals('ok', *self.resolveCode(skip=True))
+        self.assertResult([])
+
+    def testConflictWithInstalled(self):
+        ''' update fails, because it conflicts with installed
+        foo 1.0 -> 2.0 update fails, because foo-2.0 conflict with bar-1.0
+        the update get skipped and the transaction is now empty
+        '''
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po2 = FakePackage('foo', '2', '0', '0', 'noarch')
+        po2.addConflicts('bar', 'EQ', ('0', '1', '0'))
+
+        ipo = FakePackage('bar', '1', '0', '0', 'noarch')
+
+        self.rpmdb.addPackage(po1)
+        self.rpmdb.addPackage(ipo)
+
+        self.tsInfo.addUpdate(po2, oldpo=po1)
+        
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([ipo, po1])
+
+    def testConflictWithInstalledButUpdateExist(self):
+        ''' update fails, because conflict cant be fixed. (req. loop)
+        foo 1.0 -> 2.0 update fails, because foo-2.0 conflict with bar-1.0
+        bar-1.0 is update to bar-2.0, to solve the conflict but bar-2.0 need foo-1.0
+        so the foo & bar updates get skipped and the transaction is empty
+        '''
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po2 = FakePackage('foo', '2', '0', '0', 'noarch')
+        po2.addConflicts('bar', 'EQ', ('0', '1', '0'))
+
+        ipo = FakePackage('bar', '1', '0', '0', 'noarch')
+
+        self.rpmdb.addPackage(po1)
+        self.rpmdb.addPackage(ipo)
+
+        xpo = FakePackage('bar', '2', '0', '0', 'noarch')
+        xpo.addRequires('foo', 'EQ', ('0', '1', '0'))
+        self.xsack.addPackage(xpo)
+
+        self.tsInfo.addUpdate(po2, oldpo=po1)
+        
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([po1,ipo])
+
+    def testConflictWithInstalledButUpdateExist(self):
+        '''update fails, because conflict cant be fixed. (missing req.)
+        foo 1.0 -> 2.0 update fails, because foo-2.0 conflict with bar-1.0
+        bar-1.0 is update to bar-2.0, to solve the conflict but bar-2.0 need poo-1.0
+        there is not provided
+        So the foo & bar updates get skipped and the transaction is empty
+        '''
+        po1 = FakePackage('foo', '1', '0', '0', 'noarch')
+        po2 = FakePackage('foo', '2', '0', '0', 'noarch')
+        po2.addConflicts('bar', 'EQ', ('0', '1', '0'))
+
+        ipo = FakePackage('bar', '1', '0', '0', 'noarch')
+
+        self.rpmdb.addPackage(po1)
+        self.rpmdb.addPackage(ipo)
+
+        xpo = FakePackage('bar', '2', '0', '0', 'noarch')
+        xpo.addRequires('poo', 'EQ', ('0', '1', '0'))
+        self.xsack.addPackage(xpo)
+
+        self.tsInfo.addUpdate(po2, oldpo=po1)
+        
+        self.assertEquals('empty', *self.resolveCode(skip=True))
+        self.assertResult([po1,ipo])
+
+
+    def resolveCode(self,skip = False):
+        solver = YumBase()
+        solver.conf = FakeConf()
+        solver.conf.skip_broken = skip
+        solver.tsInfo = solver._tsInfo = self.tsInfo
+        solver.rpmdb = self.rpmdb
+        solver.pkgSack = self.xsack
+        
+        for po in self.rpmdb:
+            po.repoid = po.repo.id = "installed"
+        for po in self.xsack:
+            po.repoid = po.repo.id = "TestRepository"
+        for txmbr in solver.tsInfo:
+            if txmbr.ts_state in ('u', 'i'):
+                txmbr.po.repoid = txmbr.po.repo.id = "TestRepository"
+            else:
+                txmbr.po.repoid = txmbr.po.repo.id = "installed"
+
+        res, msg = solver.buildTransaction()
+        return self.res[res], msg
+
+def setup_logging():
+    logging.basicConfig()    
+    plainformatter = logging.Formatter("%(message)s")    
+    console_stdout = logging.StreamHandler(sys.stdout)
+    console_stdout.setFormatter(plainformatter)
+    verbose = logging.getLogger("yum.verbose")
+    verbose.propagate = False
+    verbose.addHandler(console_stdout)
+    verbose.setLevel(2)
+
diff --git a/test/testbase.py b/test/testbase.py
index bdb3e81..5b9d261 100644
--- a/test/testbase.py
+++ b/test/testbase.py
@@ -31,6 +31,7 @@ class FakeConf(object):
         self.installroot = '/'
         self.tsflags = []
         self.installonly_limit = 0
+        self.skip_broken = False
         self.disable_excludes = []
 
 class FakeRepo(object):
diff --git a/yum/__init__.py b/yum/__init__.py
index 92c8d26..98c9a11 100644
--- a/yum/__init__.py
+++ b/yum/__init__.py
@@ -580,9 +580,64 @@ class YumBase(depsolve.Depsolve):
         
         if self.tsInfo.changed:
             (rescode, restring) = self.resolveDeps()
+
+        # if depsolve failed and skipbroken is enabled
+        # The remove the broken packages from the transactions and
+        # Try another depsolve
+        if self.conf.skip_broken and rescode==1:
+            rescode, restring = self._skipPackagesWithProblems(rescode, restring)
             
         return rescode, restring
 
+    def _skipPackagesWithProblems(self, rescode, restring):
+        ''' Remove the packages with depsolve errors and depsolve again '''
+        # Keep removing packages & Depsolve until all errors is gone
+        # or the transaction is empty
+        depTree = self._buildDepTree()
+        while len(self.po_with_problems) > 0 and rescode == 1:
+            startTs = set(self.tsInfo)
+            toRemove = []
+            for po,wpo in self.po_with_problems:
+                # check if the problem is caused by a package in the transaction
+                if not self.tsInfo.exists(po.pkgtup):
+                    if wpo:
+                        toRemove = self._getPackagesToRemove(wpo, depTree, toRemove)
+                    else:
+                        continue
+                else:
+                    toRemove = self._getPackagesToRemove(po, depTree, toRemove)
+            if toRemove:
+                for po in toRemove:
+                    if self.tsInfo.exists(po.pkgtup):
+                        self.verbose_logger.debug("skipping %s because of depsolving problems" % str(po))
+                        self.tsInfo.remove(po.pkgtup)
+            else: # Nothing was removed, so we still got a problem
+                break # Bail out
+            rescode, restring = self.resolveDeps()
+            endTs = set(self.tsInfo)
+             # Check if tsInfo has changes since we started to skip packages
+             # if there is no changes then we got a loop.
+            if startTs-endTs == set():
+                break    # bail out
+        return rescode, restring
+
+    def _buildDepTree(self):
+        depTree = {}
+        for txmbr in self.tsInfo:
+            if not txmbr.po in depTree.keys():
+                depTree[txmbr.po] = []
+            for po in (txmbr.updates + txmbr.obsoletes+txmbr.depends_on):
+                depTree[txmbr.po].append(po)
+        return depTree
+    
+    def _getPackagesToRemove(self,po,deptree,toRemove):
+        if po not in toRemove:
+            toRemove.append(po)       
+        for child in deptree[po]:
+            if child not in toRemove:
+                toRemove = self._getPackagesToRemove(child, deptree, toRemove)
+        return toRemove
+
     def runTransaction(self, cb):
         """takes an rpm callback object, performs the transaction"""
 
diff --git a/yum/config.py b/yum/config.py
index 174118d..f1fee12 100644
--- a/yum/config.py
+++ b/yum/config.py
@@ -555,6 +555,7 @@ class YumConf(StartupConf):
     mirrorlist_expire = IntOption(86400) # time in seconds (1 day)
     rpm_check_debug = BoolOption(True)
     disable_excludes = ListOption()    
+    skip_broken = BoolOption(False)
 
     
     _reposlist = []
diff --git a/yum/depsolve.py b/yum/depsolve.py
index 25f5f76..18ddae4 100644
--- a/yum/depsolve.py
+++ b/yum/depsolve.py
@@ -258,6 +258,11 @@ class Depsolve(object):
             CheckDeps, missingdep = self._requiringFromTransaction(po, requirement, errormsgs)
         else:
             CheckDeps, missingdep = self._requiringFromInstalled(po, requirement, errormsgs)
+
+        # Check packages with problems
+        if missingdep:
+            self.po_with_problems.add((po,self._working_po))
+
         return (CheckDeps, missingdep, errormsgs)
 
     def _requiringFromInstalled(self, requiringPo, requirement, errorlist):
@@ -560,13 +565,13 @@ class Depsolve(object):
                 name))
             # FIXME: we should probably handle updating multiple packages...
             txmbr = self.tsInfo.addUpdate(best, inst[0])
-            txmbr.setAsDep()
+            txmbr.setAsDep(po=requiringPo)
             txmbr.reason = "dep"
         else:
             self.verbose_logger.debug('TSINFO: Marking %s as install for %s', best,
                 name)
             txmbr = self.tsInfo.addInstall(best)
-            txmbr.setAsDep()
+            txmbr.setAsDep(po=requiringPo)
 
             # if we had other packages with this name.arch that we found
             # before, they're not going to be installed anymore, so we
@@ -589,6 +594,7 @@ class Depsolve(object):
 
         needname, flags, needversion = conflict
         (name, arch, epoch, ver, rel) = po.pkgtup
+        requiringPo = po
 
         niceformatneed = rpmUtils.miscutils.formatRequire(needname, needversion, flags)
         if self.dsCallback: self.dsCallback.procConflict(name, niceformatneed)
@@ -640,7 +646,7 @@ class Depsolve(object):
             self.verbose_logger.log(logginglevels.DEBUG_2,
                 'TSINFO: Updating %s to resolve conflict.', po)
             txmbr = self.tsInfo.addUpdate(po, confpkg)
-            txmbr.setAsDep()
+            txmbr.setAsDep(po=requiringPo)
             txmbr.reason = "dep"
             CheckDeps = 1
             
@@ -649,7 +655,9 @@ class Depsolve(object):
             CheckDeps, conflicts = self._unresolveableConflict(conf, name, errormsgs)
             self.verbose_logger.log(logginglevels.DEBUG_1, '%s conflicts: %s',
                 name, conf)
-        
+            if conflicts:
+                self.po_with_problems.add((requiringPo,None))
+
         return (CheckDeps, conflicts, errormsgs)
 
     def _unresolveableConflict(self, conf, name, errors):
@@ -703,6 +711,8 @@ class Depsolve(object):
             # reset what we've seen as things may have changed between calls
             # to resolveDeps (rh#242368, rh#308321)
             self._dcobj.reset()
+        self.po_with_problems = set()
+        self._working_po = None
 
         CheckDeps = True
         CheckRemoves = False
@@ -770,6 +780,8 @@ class Depsolve(object):
         self.tsInfo.changed = False
         if len(errors) > 0:
             errors = unique(errors)
+            for po,wpo in self.po_with_problems:
+                self.verbose_logger.debug("%s has depsolving problems" % po)
             return (1, errors)
 
         if len(self.tsInfo) > 0:
@@ -794,6 +806,15 @@ class Depsolve(object):
             self.verbose_logger.log(logginglevels.DEBUG_2,
                                     "Checking deps for %s" %(txmbr,))
 
+            # store the primary po we currently are working on 
+            # so we can store it in self.po_with_problems.
+            # it is useful when an update is breaking an require of an installed package
+            # then we want to know who is causing the problem, not just who is having the problem. 
+            if not txmbr.updates and txmbr.relatedto:
+                self._working_po = txmbr.relatedto[0][0]
+            else:
+                self._working_po = txmbr.po
+           
             if txmbr.output_state in inst:
                 thisneeds = self._checkInstall(txmbr)
                 CheckInstalls = True
_______________________________________________
Yum-devel mailing list
[email protected]
https://lists.dulug.duke.edu/mailman/listinfo/yum-devel

Reply via email to