Add dedicated lock for clean command

Resolves: RHEL-128440
This commit is contained in:
Aleš Matěj 2026-07-14 14:20:31 +02:00
parent e56a389fa5
commit 80607cb274
5 changed files with 661 additions and 1 deletions

View File

@ -0,0 +1,59 @@
From eb465e31e4c92b6f3a54e96e097faa816957e7af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
Date: Wed, 8 Jul 2026 13:16:43 +0200
Subject: [PATCH 1/4] Add `FileLock` for read/write and blocking/non blocking
locks
ProcessLock doesn't support read only locks and it works differently: it
effectively achieves the locking by storing a PID, flock is only used to
guard writing the PID.
---
dnf/lock.py | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/dnf/lock.py b/dnf/lock.py
index 6817aac9..cb189c35 100644
--- a/dnf/lock.py
+++ b/dnf/lock.py
@@ -146,3 +146,38 @@ class ProcessLock(object):
if self.count == 1:
os.unlink(self.target)
self._unlock_thread()
+
+
+class FileLock(object):
+ """
+ A file lock with read/write access and blocking/non blocking mode.
+ """
+
+ def __init__(self, target, description, blocking=False, read=False):
+ self.blocking = blocking
+ self.read = read
+ self.description = description
+ self.target = target
+ self.fd = None
+
+ def __enter__(self):
+ dnf.util.ensure_dir(os.path.dirname(self.target))
+ self.fd = os.open(self.target, os.O_CREAT | os.O_RDWR, 0o644)
+ flags = fcntl.LOCK_SH if self.read else fcntl.LOCK_EX
+ if not self.blocking:
+ flags |= fcntl.LOCK_NB
+ try:
+ fcntl.flock(self.fd, flags)
+ except OSError as e:
+ os.close(self.fd)
+ self.fd = None
+ if e.errno == errno.EWOULDBLOCK:
+ msg = '%s already locked' % self.description
+ raise ProcessLockError(msg, -1)
+ raise
+
+ def __exit__(self, *exc_args):
+ if self.fd is not None:
+ fcntl.flock(self.fd, fcntl.LOCK_UN)
+ os.close(self.fd)
+ self.fd = None
--
2.54.0

View File

@ -0,0 +1,50 @@
From 6e320d5b7bb7dfe0fec56e2a03e9040f6dd7e42c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
Date: Tue, 7 Jul 2026 07:48:29 +0200
Subject: [PATCH 2/4] Generalize _BoolDefault to just _Default, allow choices
This is useful if we want a demand with more states than just True/False
---
dnf/cli/demand.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/dnf/cli/demand.py b/dnf/cli/demand.py
index f82a75b1..ac19930b 100644
--- a/dnf/cli/demand.py
+++ b/dnf/cli/demand.py
@@ -21,9 +21,10 @@
from __future__ import unicode_literals
-class _BoolDefault(object):
- def __init__(self, default):
+class _Default(object):
+ def __init__(self, default, choices=None):
self.default = default
+ self.choices = choices
self._storing_name = '__%s%x' % (self.__class__.__name__, id(self))
def __get__(self, obj, objtype=None):
@@ -33,6 +34,8 @@ class _BoolDefault(object):
return self.default
def __set__(self, obj, val):
+ if self.choices is not None and val not in self.choices:
+ raise ValueError('Invalid demand value: %s' % val)
objdict = obj.__dict__
if self._storing_name in objdict:
current_val = objdict[self._storing_name]
@@ -40,6 +43,10 @@ class _BoolDefault(object):
raise AttributeError('Demand already set.')
objdict[self._storing_name] = val
+
+_BoolDefault = _Default
+
+
class DemandSheet(object):
"""Collection of demands that different CLI parts have on other parts. :api"""
--
2.54.0

View File

@ -0,0 +1,214 @@
From cdff23db5f4099e344a2fcac4bc230836a3949ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
Date: Wed, 8 Jul 2026 13:47:31 +0200
Subject: [PATCH 3/4] Add clean command lock demand
---
dnf/cli/demand.py | 14 ++++++
dnf/cli/main.py | 98 +++++++++++++++++++++++-----------------
dnf/lock.py | 5 ++
tests/cli/test_demand.py | 2 +
4 files changed, 77 insertions(+), 42 deletions(-)
diff --git a/dnf/cli/demand.py b/dnf/cli/demand.py
index ac19930b..2dc3a3d4 100644
--- a/dnf/cli/demand.py
+++ b/dnf/cli/demand.py
@@ -20,6 +20,15 @@
from __future__ import unicode_literals
+from enum import Enum
+
+
+class CleanCommandLock(Enum):
+ """Clean command lock modes. :api"""
+ NONE = 0
+ READ = 1
+ WRITE = 2
+
class _Default(object):
def __init__(self, default, choices=None):
@@ -70,3 +79,8 @@ class DemandSheet(object):
# repositories packages (e.g. versionlock).
# If it stays None, the demands.resolving is used as a fallback.
plugin_filtering_enabled = _BoolDefault(None)
+
+ # Determines if/how to lock the clean command lock.
+ # This lock is used to ensure clean command (which uses WRITE mode) running concurrently
+ # with another dnf process doesn't delete required data (packages).
+ clean_command_lock = _Default(CleanCommandLock.NONE, tuple(CleanCommandLock))
diff --git a/dnf/cli/main.py b/dnf/cli/main.py
index c63baaaa..2537a46d 100644
--- a/dnf/cli/main.py
+++ b/dnf/cli/main.py
@@ -24,6 +24,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.conf import Conf
from dnf.cli.cli import Cli
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import ucd
from dnf.cli.utils import show_lock_owner
@@ -33,6 +34,7 @@ import dnf.cli
import dnf.cli.cli
import dnf.cli.option_parser
import dnf.exceptions
+import dnf.lock
import dnf.i18n
import dnf.logging
import dnf.util
@@ -43,6 +45,7 @@ import logging
import os
import os.path
import sys
+from contextlib import nullcontext
logger = logging.getLogger("dnf")
@@ -118,52 +121,63 @@ def cli_run(cli, base):
else:
f.close()
- try:
- cli.run()
- except dnf.exceptions.LockError:
- raise
- except (IOError, OSError) as e:
- return ex_IOError(e)
+ lock = None
+ if cli.demands.clean_command_lock == CleanCommandLock.NONE:
+ lock = nullcontext()
+ elif cli.demands.clean_command_lock == CleanCommandLock.READ:
+ lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, True)
+ elif cli.demands.clean_command_lock == CleanCommandLock.WRITE:
+ lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, False)
+ else:
+ raise RuntimeError('Invalid demands.clean_command_lock: %s' % cli.demands.clean_command_lock)
- if cli.demands.resolving:
+ with lock:
try:
- ret = resolving(cli, base)
- except dnf.exceptions.DepsolveError as e:
- ex_Error(e)
- msg = ""
- if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True):
- msg += _("try to add '{}' to command line to replace conflicting "
- "packages").format("--allowerasing")
- if cli.base.conf.strict:
- if not msg:
- msg += _("try to add '{}' to skip uninstallable packages").format(
- "--skip-broken")
- else:
- msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken")
- if cli.base.conf.best:
- prio = cli.base.conf._get_priority("best")
- if prio <= dnf.conf.PRIO_MAINCONFIG:
+ cli.run()
+ except dnf.exceptions.LockError:
+ raise
+ except (IOError, OSError) as e:
+ return ex_IOError(e)
+
+ if cli.demands.resolving:
+ try:
+ ret = resolving(cli, base)
+ except dnf.exceptions.DepsolveError as e:
+ ex_Error(e)
+ msg = ""
+ if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True):
+ msg += _("try to add '{}' to command line to replace conflicting "
+ "packages").format("--allowerasing")
+ if cli.base.conf.strict:
if not msg:
- msg += _("try to add '{}' to use not only best candidate packages").format(
- "--nobest")
+ msg += _("try to add '{}' to skip uninstallable packages").format(
+ "--skip-broken")
else:
- msg += _(" or '{}' to use not only best candidate packages").format(
- "--nobest")
- if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types:
- if not msg:
- msg += _("try to add '{}' to load additional filelists metadata").format(
- "--setopt=optional_metadata_types=filelists")
- else:
- msg += _(" or '{}' to load additional filelists metadata").format(
- "--setopt=optional_metadata_types=filelists")
- if msg:
- logger.info("({})".format(msg))
- raise
- if ret:
- return ret
-
- cli.command.run_transaction()
- return cli.demands.success_exit_status
+ msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken")
+ if cli.base.conf.best:
+ prio = cli.base.conf._get_priority("best")
+ if prio <= dnf.conf.PRIO_MAINCONFIG:
+ if not msg:
+ msg += _("try to add '{}' to use not only best candidate packages").format(
+ "--nobest")
+ else:
+ msg += _(" or '{}' to use not only best candidate packages").format(
+ "--nobest")
+ if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types:
+ if not msg:
+ msg += _("try to add '{}' to load additional filelists metadata").format(
+ "--setopt=optional_metadata_types=filelists")
+ else:
+ msg += _(" or '{}' to load additional filelists metadata").format(
+ "--setopt=optional_metadata_types=filelists")
+ if msg:
+ logger.info("({})".format(msg))
+ raise
+ if ret:
+ return ret
+
+ cli.command.run_transaction()
+ return cli.demands.success_exit_status
def resolving(cli, base):
diff --git a/dnf/lock.py b/dnf/lock.py
index cb189c35..aae8a51b 100644
--- a/dnf/lock.py
+++ b/dnf/lock.py
@@ -63,6 +63,11 @@ def build_log_lock(logdir, exit_on_lock):
'log', not exit_on_lock)
+def build_clean_command_lock(cachedir, exit_on_lock, read):
+ return FileLock(os.path.join(_fit_lock_dir(cachedir), 'clean_command.lock'),
+ 'clean command', not exit_on_lock, read)
+
+
class ProcessLock(object):
def __init__(self, target, description, blocking=False):
self.blocking = blocking
diff --git a/tests/cli/test_demand.py b/tests/cli/test_demand.py
index fc9cfcec..7f99100a 100644
--- a/tests/cli/test_demand.py
+++ b/tests/cli/test_demand.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import dnf.cli.demand
+from dnf.cli.demand import CleanCommandLock
import tests.support
@@ -42,6 +43,7 @@ class DemandTest(tests.support.TestCase):
self.assertFalse(demands.sack_activation)
self.assertFalse(demands.root_user)
self.assertEqual(demands.success_exit_status, 0)
+ self.assertEqual(demands.clean_command_lock, CleanCommandLock.NONE)
def test_independence(self):
d1 = dnf.cli.demand.DemandSheet()
--
2.54.0

View File

@ -0,0 +1,330 @@
From 501b7cfcd16808de7a011519e2e747ce19abab05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= <amatej@redhat.com>
Date: Wed, 8 Jul 2026 14:19:29 +0200
Subject: [PATCH 4/4] Configure `demands.clean_command_lock` for commands that
need it
---
dnf/cli/commands/__init__.py | 7 +++++++
dnf/cli/commands/clean.py | 4 ++++
dnf/cli/commands/distrosync.py | 2 ++
dnf/cli/commands/downgrade.py | 2 ++
dnf/cli/commands/group.py | 2 ++
dnf/cli/commands/history.py | 3 +++
dnf/cli/commands/install.py | 2 ++
dnf/cli/commands/module.py | 4 ++++
dnf/cli/commands/reinstall.py | 2 ++
dnf/cli/commands/shell.py | 2 ++
dnf/cli/commands/swap.py | 2 ++
dnf/cli/commands/upgrade.py | 2 ++
12 files changed, 34 insertions(+)
diff --git a/dnf/cli/commands/__init__.py b/dnf/cli/commands/__init__.py
index 52e6a033..020c0c6f 100644
--- a/dnf/cli/commands/__init__.py
+++ b/dnf/cli/commands/__init__.py
@@ -24,6 +24,7 @@ Classes for subcommands of the yum command line interface.
from __future__ import print_function
from __future__ import unicode_literals
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import _
@@ -343,6 +344,7 @@ class RepoPkgsCommand(Command):
demands.sack_activation = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_repo(self):
self.cli._populate_update_security_filter(self.opts)
@@ -395,6 +397,7 @@ class RepoPkgsCommand(Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_repo(self):
"""Execute the command with respect to given arguments *cli_args*."""
@@ -450,6 +453,7 @@ class RepoPkgsCommand(Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_repo(self):
"""Execute the command with respect to given arguments *cli_args*."""
@@ -538,6 +542,7 @@ class RepoPkgsCommand(Command):
demands.sack_activation = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def _replace(self, pkg_spec, reponame):
"""Synchronize a package with another repository or remove it."""
@@ -600,6 +605,7 @@ class RepoPkgsCommand(Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_repo(self):
"""Execute the command with respect to given arguments *cli_args*."""
@@ -689,6 +695,7 @@ class RepoPkgsCommand(Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_repo(self):
"""Execute the command with respect to given arguments *cli_args*."""
diff --git a/dnf/cli/commands/clean.py b/dnf/cli/commands/clean.py
index 77f83f02..df14ebd7 100644
--- a/dnf/cli/commands/clean.py
+++ b/dnf/cli/commands/clean.py
@@ -21,6 +21,7 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.i18n import _, P_
from dnf.yum import misc
@@ -92,6 +93,9 @@ class CleanCommand(commands.Command):
choices=_CACHE_TYPES.keys(),
help=_('Metadata type to clean'))
+ def configure(self):
+ self.cli.demands.clean_command_lock = CleanCommandLock.WRITE
+
def run(self):
cachedir = self.base.conf.cachedir
md_lock = dnf.lock.build_metadata_lock(cachedir, True)
diff --git a/dnf/cli/commands/distrosync.py b/dnf/cli/commands/distrosync.py
index 3d472e5f..7896f61f 100644
--- a/dnf/cli/commands/distrosync.py
+++ b/dnf/cli/commands/distrosync.py
@@ -20,6 +20,7 @@
from __future__ import absolute_import
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.i18n import _
@@ -41,6 +42,7 @@ class DistroSyncCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
commands._checkGPGKey(self.base, self.cli)
commands._checkEnabledRepo(self.base, self.opts.package)
diff --git a/dnf/cli/commands/downgrade.py b/dnf/cli/commands/downgrade.py
index 9e27962b..187e56d5 100644
--- a/dnf/cli/commands/downgrade.py
+++ b/dnf/cli/commands/downgrade.py
@@ -21,6 +21,7 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import _
@@ -44,6 +45,7 @@ class DowngradeCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
commands._checkGPGKey(self.base, self.cli)
if not self.opts.filenames:
diff --git a/dnf/cli/commands/group.py b/dnf/cli/commands/group.py
index 6de8baa1..9375f81d 100644
--- a/dnf/cli/commands/group.py
+++ b/dnf/cli/commands/group.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.comps import CompsQuery
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.i18n import _, ucd
import libdnf.transaction
@@ -363,6 +364,7 @@ class GroupCommand(commands.Command):
if cmd in ('install', 'upgrade'):
commands._checkGPGKey(self.base, self.cli)
+ demands.clean_command_lock = CleanCommandLock.READ
def run(self):
cmd = self.opts.subcmd
diff --git a/dnf/cli/commands/history.py b/dnf/cli/commands/history.py
index 21d04a1a..d01858b3 100644
--- a/dnf/cli/commands/history.py
+++ b/dnf/cli/commands/history.py
@@ -25,6 +25,7 @@ import hawkey
from dnf.i18n import _, ucd
from dnf.cli import commands
from dnf.transaction_sr import TransactionReplay, serialize_transaction
+from dnf.cli.demand import CleanCommandLock
import dnf.cli
import dnf.exceptions
@@ -110,6 +111,7 @@ class HistoryCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
# Override configuration options that affect how the transaction is resolved
self.base.conf.clean_requirements_on_remove = False
@@ -124,6 +126,7 @@ class HistoryCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
self._require_one_transaction_id = True
if not self.opts.transactions:
diff --git a/dnf/cli/commands/install.py b/dnf/cli/commands/install.py
index b4762ec2..8bc96eff 100644
--- a/dnf/cli/commands/install.py
+++ b/dnf/cli/commands/install.py
@@ -29,6 +29,7 @@ import hawkey
import dnf.exceptions
import dnf.util
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import _
@@ -63,6 +64,7 @@ class InstallCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
if dnf.util._is_file_pattern_present(self.opts.pkg_specs):
self.base.conf.optional_metadata_types += ["filelists"]
diff --git a/dnf/cli/commands/module.py b/dnf/cli/commands/module.py
index 88dc8b23..c03f66b0 100644
--- a/dnf/cli/commands/module.py
+++ b/dnf/cli/commands/module.py
@@ -20,6 +20,7 @@
from __future__ import print_function
from dnf.cli import commands, CliError
+from dnf.cli.demand import CleanCommandLock
from dnf.i18n import _
from dnf.module.exceptions import NoModuleException
from dnf.util import logger
@@ -210,6 +211,7 @@ class ModuleCommand(commands.Command):
demands.sack_activation = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_module(self):
try:
@@ -231,6 +233,7 @@ class ModuleCommand(commands.Command):
demands.sack_activation = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
def run_on_module(self):
module_specs = self.module_base.upgrade(self.opts.module_spec)
@@ -285,6 +288,7 @@ class ModuleCommand(commands.Command):
demands.sack_activation = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
self.base.conf.module_stream_switch = True
def run_on_module(self):
diff --git a/dnf/cli/commands/reinstall.py b/dnf/cli/commands/reinstall.py
index 2b3ceac7..9d95cea0 100644
--- a/dnf/cli/commands/reinstall.py
+++ b/dnf/cli/commands/reinstall.py
@@ -21,6 +21,7 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import _
@@ -54,6 +55,7 @@ class ReinstallCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
commands._checkGPGKey(self.base, self.cli)
if not self.opts.filenames:
commands._checkEnabledRepo(self.base)
diff --git a/dnf/cli/commands/shell.py b/dnf/cli/commands/shell.py
index 18c886ff..f2a1bf32 100644
--- a/dnf/cli/commands/shell.py
+++ b/dnf/cli/commands/shell.py
@@ -19,6 +19,7 @@
#
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.i18n import _, ucd
import dnf.util
@@ -39,6 +40,7 @@ class ShellDemandSheet(object):
resolving = True
root_user = True
sack_activation = True
+ clean_command_lock = CleanCommandLock.READ
class ShellCommand(commands.Command, cmd.Cmd):
diff --git a/dnf/cli/commands/swap.py b/dnf/cli/commands/swap.py
index d44b3f4f..e61a24dc 100644
--- a/dnf/cli/commands/swap.py
+++ b/dnf/cli/commands/swap.py
@@ -20,6 +20,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals
from dnf.i18n import _
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
import dnf.util
import logging
@@ -47,6 +48,7 @@ class SwapCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
commands._checkGPGKey(self.base, self.cli)
commands._checkEnabledRepo(self.base, [self.opts.install_spec])
diff --git a/dnf/cli/commands/upgrade.py b/dnf/cli/commands/upgrade.py
index 7697fb27..7c5db7df 100644
--- a/dnf/cli/commands/upgrade.py
+++ b/dnf/cli/commands/upgrade.py
@@ -27,6 +27,7 @@ import dnf.exceptions
import dnf.base
import dnf.util
from dnf.cli import commands
+from dnf.cli.demand import CleanCommandLock
from dnf.cli.option_parser import OptionParser
from dnf.i18n import _
@@ -57,6 +58,7 @@ class UpgradeCommand(commands.Command):
demands.available_repos = True
demands.resolving = True
demands.root_user = True
+ demands.clean_command_lock = CleanCommandLock.READ
if dnf.util._is_file_pattern_present(self.opts.pkg_specs):
self.base.conf.optional_metadata_types += ["filelists"]
--
2.54.0

View File

@ -72,7 +72,7 @@ It supports RPMs, modules and comps groups & environments.
Name: dnf
Version: 4.20.0
Release: 25%{?dist}
Release: 26%{?dist}
Summary: %{pkg_summary}
# For a breakdown of the licensing, see PACKAGE-LICENSING
License: GPL-2.0-or-later AND GPL-1.0-only
@ -122,6 +122,10 @@ Patch41: 0041-bootc-Call-make_writable-when-DeploymentUnlockedStat.patch
Patch42: 0042-Preserve-ACL-when-rotating-logs.patch
Patch43: 0043-history-info-Fix-Persistence-display-for-interval-qu.patch
Patch44: 0044-automatic-Add-systemd-inhibitor-lock.patch
Patch45: 0045-Add-FileLock-for-read-write-and-blocking-non-blockin.patch
Patch46: 0046-Generalize-_BoolDefault-to-just-_Default-allow-choic.patch
Patch47: 0047-Add-clean-command-lock-demand.patch
Patch48: 0048-Configure-demands.clean_command_lock-for-commands-th.patch
BuildArch: noarch
BuildRequires: cmake
@ -483,6 +487,9 @@ popd
# bootc subpackage does not include any files
%changelog
* Tue Jul 14 2026 Ales Matej <amatej@redhat.com> - 4.20.0-26
- Add dedicated lock for clean command (RHEL-128440)
* Wed Jul 08 2026 Marek Blaha <mblaha@redhat.com> - 4.20.0-25
- automatic: Add systemd inhibitor lock (RHEL-111536)