Backport PR #1038: make compose fail on missing group packages
This commit is contained in:
parent
463d813b02
commit
1a4e2c490c
420
0001-Restore-strict-choice-for-group-installs-1461539.patch
Normal file
420
0001-Restore-strict-choice-for-group-installs-1461539.patch
Normal file
@ -0,0 +1,420 @@
|
||||
From edbd86922b2733fd36622abadca15e744d12bfde Mon Sep 17 00:00:00 2001
|
||||
From: Adam Williamson <awilliam@redhat.com>
|
||||
Date: Tue, 13 Mar 2018 15:33:24 -0700
|
||||
Subject: [PATCH] Restore 'strict' choice for group installs (#1461539)
|
||||
|
||||
This makes the `strict` parameter to `group_install` mean
|
||||
something again.
|
||||
|
||||
When it is set `True`, DNF will behave as yum previously did,
|
||||
which has been a request for several years now. See:
|
||||
|
||||
https://bugzilla.redhat.com/show_bug.cgi?id=1292892
|
||||
https://bugzilla.redhat.com/show_bug.cgi?id=1337731
|
||||
https://bugzilla.redhat.com/show_bug.cgi?id=1427365
|
||||
https://bugzilla.redhat.com/show_bug.cgi?id=1461539
|
||||
|
||||
And commits:
|
||||
|
||||
91f9ce98dbb630800017f44180d636af395435cd
|
||||
1e1901822748b754d038dd396655c997281a7201
|
||||
|
||||
We have been around the houses on this multiple times. @mikem23
|
||||
actually got things right way way back in #1292892:
|
||||
|
||||
"yum-deprecated behaves more sanely here.
|
||||
|
||||
In case 1 [package exists but has dependency issues], it reports
|
||||
the missing dependency and exits with an error
|
||||
|
||||
...
|
||||
|
||||
In case 2 [package cannot be found at all], it exits cleanly, but
|
||||
prints a warning."
|
||||
|
||||
Note that this applies to yum's *default* behaviour: it had an
|
||||
option, --skip-broken, which caused it to skip packages with
|
||||
dependency errors.
|
||||
|
||||
That's exactly what we've wanted all along, and it's also what
|
||||
1461539 requests. Unfortunately, the initial commit intended to
|
||||
fix #1292892 - that's 91f9ce9 - did not quite behave as Mike
|
||||
requested. It treated any 'mandatory' package having dependency
|
||||
errors *or* not existing at all as fatal errors, while not
|
||||
treating non-existence *or* errors on the part of a 'default'
|
||||
or 'optional' package as a fatal error. This introduced *two*
|
||||
differences from yum's behaviour (making the comps 'type' matter,
|
||||
which it never did to yum, and failing on non-existence), which
|
||||
I think was the source of quite a lot of confusion.
|
||||
|
||||
We then wound up filing bugs on this - like #1337731 - and the
|
||||
response to that was 1e19018, which causes dnf to treat *neither*
|
||||
case as a fatal error for *any* package, which is where we stand
|
||||
at present.
|
||||
|
||||
When resolving a transaction, currently, if it cannot find a
|
||||
package corresponding to an entry in a group, dnf will log a
|
||||
warning and carry on. This is good and fine and just what we want
|
||||
it to do. However, when a package is available but cannot be
|
||||
installed, it does the wrong thing. The various forks of this
|
||||
`trans_install` internal function all wind up passing
|
||||
`optional=True` to the Goal, which tells it that it's fine to
|
||||
just leave the package out if its dependencies can't be resolved.
|
||||
Additionally, if this happens, `resolve()` itself does not log
|
||||
anything.
|
||||
|
||||
If you try a group install through the CLI, it will tell you
|
||||
about packages that have been left out due to dependency errors,
|
||||
via the `_skipped_packages` function in `dnf/output.py` which
|
||||
actually sucks them out of the Goal instance itself. But that's
|
||||
no use to anything besides the CLI. Other things which use
|
||||
`resolve()`, like anaconda, have no means of accessing this
|
||||
information, so when this happens to them, the omission of the
|
||||
package is not logged in any way at all, and they have no way to
|
||||
find out about it.
|
||||
|
||||
If `strict` is set to `False`, the current behaviour described
|
||||
above will be used: non-installable packages will be skipped. We
|
||||
implement this by restoring the old separation between `install`
|
||||
and `install_opt` which was removed in 1e19018. When group_install
|
||||
is called with `strict=True` we add the packages to the `install`
|
||||
set; when it's called with `strict=False` we add the packages to
|
||||
the `install_opt` set. Then `_finalize_comps_trans` requests
|
||||
strict behaviour for `install` and non-strict for `install_opt`.
|
||||
|
||||
It continues to be the case that the comps 'type' - mandatory,
|
||||
default, or optional - does not matter; the same behaviour will be
|
||||
used for all three. Again, this is how yum behaved (in the
|
||||
absence of --skip-broken)
|
||||
|
||||
The comps 'types' were *never* intended to dictate yum/dnf
|
||||
behaviour in terms of whether it should fail on error or not, as I
|
||||
understand it; they were in fact related to installer UI behaviour.
|
||||
In the old installer UI, after you selected a group, you would see
|
||||
all the packages in the group listed. 'mandatory' packages would
|
||||
be pre-checked for installation and the check would be greyed out
|
||||
- you couldn't unselect them. 'default' packages would be
|
||||
pre-checked for installation, but you *could* de-select them if you
|
||||
wanted. 'optional' packages would not be pre-checked for
|
||||
installation, but you could *select* them if you wanted. This was
|
||||
the intent of the comps 'type', as I understand it.
|
||||
|
||||
Importantly, a package not existing at all will continue to *not*
|
||||
be a fatal error whatever `strict` is set to. as this is handled
|
||||
*before* we reach the `trans_install` function.
|
||||
|
||||
Signed-off-by: Adam Williamson <awilliam@redhat.com>
|
||||
---
|
||||
dnf/base.py | 21 ++++--
|
||||
dnf/comps.py | 27 +++++++-
|
||||
tests/repos/broken_group.repo | 4 ++
|
||||
tests/repos/main_comps.xml | 4 +-
|
||||
tests/test_groups.py | 122 +++++++++++++++++++++++++++-------
|
||||
5 files changed, 145 insertions(+), 33 deletions(-)
|
||||
create mode 100644 tests/repos/broken_group.repo
|
||||
|
||||
diff --git a/dnf/base.py b/dnf/base.py
|
||||
index 88e9467a..dacc42d9 100644
|
||||
--- a/dnf/base.py
|
||||
+++ b/dnf/base.py
|
||||
@@ -1450,17 +1450,17 @@ class Base(object):
|
||||
self._goal.upgrade(select=sltr)
|
||||
return remove_query
|
||||
|
||||
- def trans_install(query, remove_query, comps_pkg):
|
||||
+ def trans_install(query, remove_query, comps_pkg, strict):
|
||||
if self.conf.multilib_policy == "all":
|
||||
if not comps_pkg.requires:
|
||||
- self._install_multiarch(query, strict=False)
|
||||
+ self._install_multiarch(query, strict=strict)
|
||||
else:
|
||||
# it installs only one arch for conditional packages
|
||||
installed_query = query.installed().apply()
|
||||
self._report_already_installed(installed_query)
|
||||
sltr = dnf.selector.Selector(self.sack)
|
||||
sltr.set(provides="({} if {})".format(comps_pkg.name, comps_pkg.requires))
|
||||
- self._goal.install(select=sltr, optional=True)
|
||||
+ self._goal.install(select=sltr, optional=not strict)
|
||||
|
||||
else:
|
||||
sltr = dnf.selector.Selector(self.sack)
|
||||
@@ -1468,7 +1468,7 @@ class Base(object):
|
||||
sltr.set(provides="({} if {})".format(comps_pkg.name, comps_pkg.requires))
|
||||
else:
|
||||
sltr.set(pkg=query)
|
||||
- self._goal.install(select=sltr, optional=True)
|
||||
+ self._goal.install(select=sltr, optional=not strict)
|
||||
return remove_query
|
||||
|
||||
def trans_remove(query, remove_query, comps_pkg):
|
||||
@@ -1476,7 +1476,8 @@ class Base(object):
|
||||
return remove_query
|
||||
|
||||
remove_query = self.sack.query().filterm(empty=True)
|
||||
- attr_fn = ((trans.install, trans_install),
|
||||
+ attr_fn = ((trans.install, functools.partial(trans_install, strict=True)),
|
||||
+ (trans.install_opt, functools.partial(trans_install, strict=False)),
|
||||
(trans.upgrade, trans_upgrade),
|
||||
(trans.remove, trans_remove))
|
||||
|
||||
@@ -1547,6 +1548,10 @@ class Base(object):
|
||||
"""Installs packages of selected group
|
||||
:param exclude: list of package name glob patterns
|
||||
that will be excluded from install set
|
||||
+ :param strict: boolean indicating whether group packages that
|
||||
+ exist but are non-installable due to e.g. dependency
|
||||
+ issues should be skipped (False) or cause transaction to
|
||||
+ fail to resolve (True)
|
||||
"""
|
||||
def _pattern_to_pkgname(pattern):
|
||||
if dnf.util.is_glob_pattern(pattern):
|
||||
@@ -1568,8 +1573,12 @@ class Base(object):
|
||||
strict)
|
||||
if not trans:
|
||||
return 0
|
||||
+ if strict:
|
||||
+ instlog = trans.install
|
||||
+ else:
|
||||
+ instlog = trans.install_opt
|
||||
logger.debug(_("Adding packages from group '%s': %s"),
|
||||
- grp_id, trans.install)
|
||||
+ grp_id, instlog)
|
||||
return self._add_comps_trans(trans)
|
||||
|
||||
def env_group_install(self, patterns, types, strict=True, exclude=None, exclude_groups=None):
|
||||
diff --git a/dnf/comps.py b/dnf/comps.py
|
||||
index 079d21be..8743812a 100644
|
||||
--- a/dnf/comps.py
|
||||
+++ b/dnf/comps.py
|
||||
@@ -476,18 +476,20 @@ class CompsTransPkg(object):
|
||||
class TransactionBunch(object):
|
||||
def __init__(self):
|
||||
self._install = set()
|
||||
+ self._install_opt = set()
|
||||
self._remove = set()
|
||||
self._upgrade = set()
|
||||
|
||||
def __iadd__(self, other):
|
||||
self._install.update(other._install)
|
||||
+ self._install_opt.update(other._install_opt)
|
||||
self._upgrade.update(other._upgrade)
|
||||
self._remove = (self._remove | other._remove) - \
|
||||
- self._install - self._upgrade
|
||||
+ self._install - self._install_opt - self._upgrade
|
||||
return self
|
||||
|
||||
def __len__(self):
|
||||
- return len(self.install) + len(self.upgrade) + len(self.remove)
|
||||
+ return len(self.install) + len(self.install_opt) + len(self.upgrade) + len(self.remove)
|
||||
|
||||
@staticmethod
|
||||
def _set_value(param, val):
|
||||
@@ -499,12 +501,28 @@ class TransactionBunch(object):
|
||||
|
||||
@property
|
||||
def install(self):
|
||||
+ """
|
||||
+ Packages to be installed with strict=True - transaction will
|
||||
+ fail if they cannot be installed due to dependency errors etc.
|
||||
+ """
|
||||
return self._install
|
||||
|
||||
@install.setter
|
||||
def install(self, value):
|
||||
self._set_value(self._install, value)
|
||||
|
||||
+ @property
|
||||
+ def install_opt(self):
|
||||
+ """
|
||||
+ Packages to be installed with strict=False - they will be
|
||||
+ skipped if they cannot be installed
|
||||
+ """
|
||||
+ return self._install_opt
|
||||
+
|
||||
+ @install_opt.setter
|
||||
+ def install_opt(self, value):
|
||||
+ self._set_value(self._install_opt, value)
|
||||
+
|
||||
@property
|
||||
def remove(self):
|
||||
return self._remove
|
||||
@@ -641,7 +659,10 @@ class Solver(object):
|
||||
|
||||
trans = TransactionBunch()
|
||||
# TODO: remove exclude
|
||||
- trans.install.update(self._pkgs_of_type(comps_group, pkg_types, exclude=[]))
|
||||
+ if strict:
|
||||
+ trans.install.update(self._pkgs_of_type(comps_group, pkg_types, exclude=[]))
|
||||
+ else:
|
||||
+ trans.install_opt.update(self._pkgs_of_type(comps_group, pkg_types, exclude=[]))
|
||||
return trans
|
||||
|
||||
def _group_remove(self, group_id):
|
||||
diff --git a/tests/repos/broken_group.repo b/tests/repos/broken_group.repo
|
||||
new file mode 100644
|
||||
index 00000000..7f151971
|
||||
--- /dev/null
|
||||
+++ b/tests/repos/broken_group.repo
|
||||
@@ -0,0 +1,4 @@
|
||||
+=Ver: 2.0
|
||||
+#
|
||||
+=Pkg: brokendeps 20 2 x86_64
|
||||
+=Req: nosuchpackage >= 1.2-0
|
||||
diff --git a/tests/repos/main_comps.xml b/tests/repos/main_comps.xml
|
||||
index 3cf8faa5..9e694d13 100644
|
||||
--- a/tests/repos/main_comps.xml
|
||||
+++ b/tests/repos/main_comps.xml
|
||||
@@ -44,7 +44,9 @@
|
||||
<name>Broken Group</name>
|
||||
<packagelist>
|
||||
<packagereq type="mandatory">meaning-of-life</packagereq>
|
||||
- <packagereq>lotus</packagereq>
|
||||
+ <packagereq type="mandatory">lotus</packagereq>
|
||||
+ <packagereq type="default" requires="no-such-package">librita</packagereq>
|
||||
+ <packagereq type="optional">brokendeps</packagereq>
|
||||
</packagelist>
|
||||
</group>
|
||||
<category>
|
||||
diff --git a/tests/test_groups.py b/tests/test_groups.py
|
||||
index ec10a619..fe388f96 100644
|
||||
--- a/tests/test_groups.py
|
||||
+++ b/tests/test_groups.py
|
||||
@@ -183,29 +183,6 @@ class PresetPersistorTest(tests.support.ResultTestCase):
|
||||
swdb_group = self.history.group.get(comps_group.id)
|
||||
self.assertIsNotNone(swdb_group)
|
||||
|
||||
- """
|
||||
- this should be reconsidered once relengs document comps
|
||||
- def test_group_install_broken(self):
|
||||
- grp = self.base.comps.group_by_pattern('Broken Group')
|
||||
- p_grp = self.history.group.get('broken-group')
|
||||
- self.assertFalse(p_grp.installed)
|
||||
-
|
||||
- self.assertRaises(dnf.exceptions.MarkingError,
|
||||
- self.base.group_install, grp.id,
|
||||
- ('mandatory', 'default'))
|
||||
- p_grp = self.history.group.get('broken-group')
|
||||
- self.assertFalse(p_grp.installed)
|
||||
-
|
||||
- self.assertEqual(self.base.group_install(grp.id,
|
||||
- ('mandatory', 'default'),
|
||||
- strict=False), 1)
|
||||
- inst, removed = self.installed_removed(self.base)
|
||||
- self.assertLength(inst, 1)
|
||||
- self.assertEmpty(removed)
|
||||
- p_grp = self.history.group.get('broken-group')
|
||||
- self.assertTrue(p_grp.installed)
|
||||
- """
|
||||
-
|
||||
def test_group_remove(self):
|
||||
self._install_test_group()
|
||||
group_id = 'somerset'
|
||||
@@ -220,6 +197,105 @@ class PresetPersistorTest(tests.support.ResultTestCase):
|
||||
self._swdb_end()
|
||||
|
||||
|
||||
+class ProblemGroupTest(tests.support.ResultTestCase):
|
||||
+ """Test some cases involving problems in groups: packages that
|
||||
+ don't exist, and packages that exist but cannot be installed. The
|
||||
+ "broken" group lists three packages. "meaning-of-life", explicitly
|
||||
+ 'default', does not exist. "lotus", implicitly 'mandatory' (no
|
||||
+ explicit type), exists and is installable. "brokendeps",
|
||||
+ explicitly 'optional', exists but has broken dependencies. See
|
||||
+ https://bugzilla.redhat.com/show_bug.cgi?id=1292892,
|
||||
+ https://bugzilla.redhat.com/show_bug.cgi?id=1337731,
|
||||
+ https://bugzilla.redhat.com/show_bug.cgi?id=1427365, and
|
||||
+ https://bugzilla.redhat.com/show_bug.cgi?id=1461539 for some of
|
||||
+ the background on this.
|
||||
+ """
|
||||
+
|
||||
+ REPOS = ['main', 'broken_group']
|
||||
+ COMPS = True
|
||||
+ COMPS_SEED_PERSISTOR = True
|
||||
+
|
||||
+ def test_group_install_broken_mandatory(self):
|
||||
+ """Here we will test installing the group with only mandatory
|
||||
+ packages. We expect this to succeed, leaving out the
|
||||
+ non-existent 'meaning-of-life': it should also log a warning,
|
||||
+ but we don't test that.
|
||||
+ """
|
||||
+ comps_group = self.base.comps.group_by_pattern('Broken Group')
|
||||
+ swdb_group = self.history.group.get(comps_group.id)
|
||||
+ self.assertIsNone(swdb_group)
|
||||
+
|
||||
+ cnt = self.base.group_install(comps_group.id, ('mandatory'))
|
||||
+ self._swdb_commit()
|
||||
+ self.base.resolve()
|
||||
+ # this counts packages *listed* in the group, so 2
|
||||
+ self.assertEqual(cnt, 2)
|
||||
+
|
||||
+ inst, removed = self.installed_removed(self.base)
|
||||
+ # the above should work, but only 'lotus' actually installed
|
||||
+ self.assertLength(inst, 1)
|
||||
+ self.assertEmpty(removed)
|
||||
+
|
||||
+ def test_group_install_broken_default(self):
|
||||
+ """Here we will test installing the group with only mandatory
|
||||
+ and default packages. Again we expect this to succeed: the new
|
||||
+ factor is an entry pulling in librita if no-such-package is
|
||||
+ also included or installed. We expect this not to actually
|
||||
+ pull in librita (as no-such-package obviously *isn't* there),
|
||||
+ but also not to cause a fatal error.
|
||||
+ """
|
||||
+ comps_group = self.base.comps.group_by_pattern('Broken Group')
|
||||
+ swdb_group = self.history.group.get(comps_group.id)
|
||||
+ self.assertIsNone(swdb_group)
|
||||
+
|
||||
+ cnt = self.base.group_install(comps_group.id, ('mandatory', 'default'))
|
||||
+ self._swdb_commit()
|
||||
+ self.base.resolve()
|
||||
+ # this counts packages *listed* in the group, so 3
|
||||
+ self.assertEqual(cnt, 3)
|
||||
+
|
||||
+ inst, removed = self.installed_removed(self.base)
|
||||
+ # the above should work, but only 'lotus' actually installed
|
||||
+ self.assertLength(inst, 1)
|
||||
+ self.assertEmpty(removed)
|
||||
+
|
||||
+ def test_group_install_broken_optional(self):
|
||||
+ """Here we test installing the group with optional packages
|
||||
+ included. We expect this to fail, as a package that exists but
|
||||
+ has broken dependencies is now included.
|
||||
+ """
|
||||
+ comps_group = self.base.comps.group_by_pattern('Broken Group')
|
||||
+ swdb_group = self.history.group.get(comps_group.id)
|
||||
+ self.assertIsNone(swdb_group)
|
||||
+
|
||||
+ cnt = self.base.group_install(comps_group.id, ('mandatory', 'default', 'optional'))
|
||||
+ self.assertEqual(cnt, 4)
|
||||
+
|
||||
+ self._swdb_commit()
|
||||
+ # this should fail, as optional 'brokendeps' is now pulled in
|
||||
+ self.assertRaises(dnf.exceptions.DepsolveError, self.base.resolve)
|
||||
+
|
||||
+ def test_group_install_broken_optional_nonstrict(self):
|
||||
+ """Here we test installing the group with optional packages
|
||||
+ included, but with strict=False. We expect this to succeed,
|
||||
+ skipping the package with broken dependencies.
|
||||
+ """
|
||||
+ comps_group = self.base.comps.group_by_pattern('Broken Group')
|
||||
+ swdb_group = self.history.group.get(comps_group.id)
|
||||
+ self.assertIsNone(swdb_group)
|
||||
+
|
||||
+ cnt = self.base.group_install(comps_group.id, ('mandatory', 'default', 'optional'),
|
||||
+ strict=False)
|
||||
+ self._swdb_commit()
|
||||
+ self.base.resolve()
|
||||
+ self.assertEqual(cnt, 4)
|
||||
+
|
||||
+ inst, removed = self.installed_removed(self.base)
|
||||
+ # the above should work, but only 'lotus' actually installed
|
||||
+ self.assertLength(inst, 1)
|
||||
+ self.assertEmpty(removed)
|
||||
+
|
||||
+
|
||||
class EnvironmentInstallTest(tests.support.ResultTestCase):
|
||||
"""Set up a test where sugar is considered not installed."""
|
||||
|
||||
--
|
||||
2.19.0
|
||||
|
8
dnf.spec
8
dnf.spec
@ -72,12 +72,15 @@ It supports RPMs, modules and comps groups & environments.
|
||||
|
||||
Name: dnf
|
||||
Version: 3.5.1
|
||||
Release: 1%{?dist}
|
||||
Release: 2%{?dist}
|
||||
Summary: %{pkg_summary}
|
||||
# For a breakdown of the licensing, see PACKAGE-LICENSING
|
||||
License: GPLv2+ and GPLv2 and GPL
|
||||
URL: https://github.com/rpm-software-management/dnf
|
||||
Source0: %{url}/archive/%{version}/%{name}-%{version}.tar.gz
|
||||
# Backport of https://github.com/rpm-software-management/dnf/pull/1038
|
||||
# Should make compose fail on missing group packages again
|
||||
Patch0: 0001-Restore-strict-choice-for-group-installs-1461539.patch
|
||||
BuildArch: noarch
|
||||
BuildRequires: cmake
|
||||
BuildRequires: gettext
|
||||
@ -487,6 +490,9 @@ ln -sr %{buildroot}%{confdir}/vars %{buildroot}%{_sysconfdir}/yum/vars
|
||||
%endif
|
||||
|
||||
%changelog
|
||||
* Thu Sep 20 2018 Adam Williamson <awilliam@redhat.com> - 3.5.1-2
|
||||
- Backport PR #1038 to make compose fail on missing group packages again
|
||||
|
||||
* Mon Sep 10 2018 Jaroslav Mracek <jmracek@redhat.com> - 3.5.1-1
|
||||
- [module] Fixed list and info subcommands
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user