Tim Lauridsen wrote:
Tim Lauridsen wrote:
Florian Festi wrote:
Hi Tim!

Some more skip broken code:

Fix the endless loop (sorry, test case is still broken). Code is not that beautiful - feel free to adjust your personal taste ;)=

Tim Lauridsen wrote:
Added some patch to fix issues with skip-broken and updates.

If we remove an update, we have to remove the cleanup txmbr of the package being updated.
There is also a patch to fix the testcases.

Add support for obsoletes.

Have fun

Florian


------------------------------------------------------------------------

_______________________________________________
Yum-devel mailing list
[email protected]
https://lists.dulug.duke.edu/mailman/listinfo/yum-devel

Thanks,
To not loose track of all the changes, i have created a patch with all the changes against the current yum HEAD.

Tim



------------------------------------------------------------------------

_______________________________________________
Yum-devel mailing list
[email protected]
https://lists.dulug.duke.edu/mailman/listinfo/yum-devel

I have reworked the skip-broken code and added some extra test cases.

I have added a patch, with the diff from yum HEAD.

The patch includes, the 'Make the removal test case easier by ffesti" to make some removal test cases, work right.

Tim


------------------------------------------------------------------------

_______________________________________________
Yum-devel mailing list
[email protected]
https://lists.dulug.duke.edu/mailman/listinfo/yum-devel

Now it works even better and handle updates breaking requirement of installed packages, a common real world situation in rawhide.

Tim
diff --git a/test/depsolvetests.py b/test/depsolvetests.py
index 22efc44..455fb19 100644
--- a/test/depsolvetests.py
+++ b/test/depsolvetests.py
@@ -445,11 +445,8 @@ class DepsolveTests(DepsolveTests):
         updatepo = FakePackage('zip', '2', '1', '0', 'i386')
         self.xsack.addPackage(updatepo)
 
-        if new_behavior:
-            self.assertEquals('ok', *self.resolveCode())
-            self.assertResult((po, updatepo))
-        else:
-            self.assertEquals('err', *self.resolveCode())
+        self.assertEquals('ok', *self.resolveCode())
+        self.assertResult((po, updatepo))
 
     def testUpdateForConflict2(self):
         po = FakePackage('zsh', '1', '1', '0', 'i386')
diff --git a/test/skipbroken-tests.py b/test/skipbroken-tests.py
new file mode 100644
index 0000000..9148faa
--- /dev/null
+++ b/test/skipbroken-tests.py
@@ -0,0 +1,294 @@
+import unittest
+import logging
+import sys
+from testbase import *
+
+class SkipBrokenTests(DepsolveTests):
+    ''' Test cases to test skip-broken'''
+
+    def testMissingReqNoSkip(self):
+        ''' 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):
+        ''' 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):
+        ''' 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):
+        ''' 
+        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):
+        '''
+        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):
+        '''
+        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 testLoop(self):
+        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 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 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):
+        '''
+        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):
+        '''
+        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):
+        '''
+        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('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 testConflictWithInstalledButUpdateExist2(self):
+        '''
+        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 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 suite():
+    if len(sys.argv) > 1:
+        if sys.argv[1] == '-v' or sys.argv[1] == '--verbose': 
+            setup_logging()
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(SkipBrokenTests))
+    return suite
+
+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)
+
+if __name__ == "__main__":
+    unittest.main(defaultTest="suite")
diff --git a/test/testbase.py b/test/testbase.py
index 3c90636..e22dcc2 100644
--- a/test/testbase.py
+++ b/test/testbase.py
@@ -35,6 +35,7 @@ class FakeConf(object):
         self.installroot = '/'
         self.tsflags = []
         self.installonly_limit = 0
+        self.skip_broken = False
 
 class FakeRepo(object):
 
diff --git a/yum/__init__.py b/yum/__init__.py
index f9618bc..c1ff521 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 e026db2..6cae880 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):
@@ -559,13 +564,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
@@ -588,6 +593,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)
@@ -639,7 +645,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
             
@@ -648,7 +654,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):
@@ -702,6 +710,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
@@ -769,6 +779,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:
@@ -793,6 +805,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
diff --git a/yum/transactioninfo.py b/yum/transactioninfo.py
index 29954c8..8aed378 100644
--- a/yum/transactioninfo.py
+++ b/yum/transactioninfo.py
@@ -153,6 +153,10 @@ class TransactionData:
 
         return result
 
+    def _isLocalPackage(self, txmember):
+        # Is this the right criteria?
+        return txmember.ts_state in ('u', 'i') and not isinstance(txmember.po, 
(YumInstalledPackage, YumAvailablePackageSqlite))
+
     def add(self, txmember):
         """add a package to the transaction"""
         
@@ -170,9 +174,7 @@ class TransactionData:
         self.pkgdict[txmember.pkgtup].append(txmember)
         self._namedict.setdefault(txmember.name, []).append(txmember)
         self.changed = True
-
-        # Is this the right criteria?
-        if not isinstance(txmember.po, (YumInstalledPackage, 
YumAvailablePackageSqlite)):
+        if self._isLocalPackage(txmember):
             self.localSack.addPackage(txmember.po)
         elif isinstance(txmember.po, YumAvailablePackageSqlite):
             self.pkgSackPackages += 1
@@ -190,8 +192,7 @@ class TransactionData:
             return
         for txmbr in self.pkgdict[pkgtup]:
             txmbr.po.state = None
-            # Is this the right criteria?
-            if not isinstance(txmbr.po, (YumInstalledPackage, 
YumAvailablePackageSqlite)):
+            if self._isLocalPackage(txmbr):
                 self.localSack.delPackage(txmbr.po)
             elif isinstance(txmbr.po, YumAvailablePackageSqlite):
                 self.pkgSackPackages -= 1
_______________________________________________
Yum-devel mailing list
[email protected]
https://lists.dulug.duke.edu/mailman/listinfo/yum-devel

Reply via email to