From 33736f2fb0018ffad21837f91cf4c59bb04d001a Mon Sep 17 00:00:00 2001 From: eabdullin Date: Thu, 27 Mar 2025 13:04:38 +0000 Subject: [PATCH] import CS dnf-4.20.0-12.el10 --- ...r-ostree-based-systems-and-warn-user.patch | 98 ++++++++ ...pdate-ostree-bootc-host-system-check.patch | 106 ++++++++ ...hosts-message-to-point-to-bootc-help.patch | 32 +++ ...nstallroot-on-read-only-bootc-system.patch | 47 ++++ ...wnloadonly-on-read-only-bootc-system.patch | 40 +++ ...ic-check-availability-of-config-file.patch | 131 ++++++++++ 0012-Add-support-for-transient.patch | 181 +++++++++++++ ...c-Document-transient-and-persistence.patch | 74 ++++++ ...-GObject-API-to-get-deployment-statu.patch | 166 ++++++++++++ ...ng-use-ostree-admin-unlock-transient.patch | 237 ++++++++++++++++++ 0016-spec-Add-dnf-bootc-subpackage.patch | 56 +++++ ...ibdnf-0.74.0-with-persistence-option.patch | 45 ++++ ...r_-major-minor-in-conf-not-substitut.patch | 162 ++++++++++++ ...eleasever_-major-minor-with-provides.patch | 179 +++++++++++++ ...r-major-and-releasever-minor-options.patch | 124 +++++++++ ...etect_releasevers-and-update-example.patch | 57 +++++ ...ct_releasevers-not-detect_releasever.patch | 129 ++++++++++ ...easever-releasever_-major-minor-affe.patch | 97 +++++++ dnf.spec | 62 ++++- 19 files changed, 2021 insertions(+), 2 deletions(-) create mode 100644 0006-Add-detection-for-ostree-based-systems-and-warn-user.patch create mode 100644 0007-Update-ostree-bootc-host-system-check.patch create mode 100644 0008-Update-bootc-hosts-message-to-point-to-bootc-help.patch create mode 100644 0009-Allow-installroot-on-read-only-bootc-system.patch create mode 100644 0010-Allow-downloadonly-on-read-only-bootc-system.patch create mode 100644 0011-Automatic-check-availability-of-config-file.patch create mode 100644 0012-Add-support-for-transient.patch create mode 100644 0013-bootc-Document-transient-and-persistence.patch create mode 100644 0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch create mode 100644 0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch create mode 100644 0016-spec-Add-dnf-bootc-subpackage.patch create mode 100644 0017-Require-libdnf-0.74.0-with-persistence-option.patch create mode 100644 0018-Derive-releasever_-major-minor-in-conf-not-substitut.patch create mode 100644 0019-Override-releasever_-major-minor-with-provides.patch create mode 100644 0020-Add-releasever-major-and-releasever-minor-options.patch create mode 100644 0021-doc-Document-detect_releasevers-and-update-example.patch create mode 100644 0022-tests-Patch-detect_releasevers-not-detect_releasever.patch create mode 100644 0023-Document-how-releasever-releasever_-major-minor-affe.patch diff --git a/0006-Add-detection-for-ostree-based-systems-and-warn-user.patch b/0006-Add-detection-for-ostree-based-systems-and-warn-user.patch new file mode 100644 index 0000000..0935268 --- /dev/null +++ b/0006-Add-detection-for-ostree-based-systems-and-warn-user.patch @@ -0,0 +1,98 @@ +From b00c7171f58dbbda3df4bf5f2e65cbc7eff37a5b Mon Sep 17 00:00:00 2001 +From: David Cantrell +Date: Thu, 15 Feb 2024 14:03:32 -0500 +Subject: [PATCH] Add detection for ostree-based systems and warn users about + losing changes +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 5c050ba2324c5fb95bf0e0501c7925f38f6a09dc + +On ostree-based systems, users can use dnf to customize the +environment but those changes will be lost at the next ostree-based +image update. If you want to retain changes between ostree-updates +you need to make use of rpm-ostree right now. + +Signed-off-by: David Cantrell +Resolves: https://issues.redhat.com/browse/RHEL-49671 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 9 +++++++++ + dnf/util.py | 31 +++++++++++++++++++++++++++++++ + 2 files changed, 40 insertions(+) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 1824bd00e..c14f83639 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -214,6 +214,15 @@ class BaseCli(dnf.Base): + elif 'test' in self.conf.tsflags: + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) ++ if dnf.util.is_container(): ++ _container_msg = _(""" ++*** This system is managed with ostree. Changes to the system ++*** made with dnf will be lost with the next ostree-based update. ++*** If you do not want to lose these changes, use 'rpm-ostree'. ++""") ++ logger.info(_container_msg) ++ raise CliError(_("Operation aborted.")) ++ + if self._promptWanted(): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) +diff --git a/dnf/util.py b/dnf/util.py +index 6cd7ad41f..1b465bda5 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -33,11 +33,13 @@ import errno + import functools + import hawkey + import itertools ++import json + import locale + import logging + import os + import pwd + import shutil ++import subprocess + import sys + import tempfile + import time +@@ -639,3 +641,32 @@ def _is_file_pattern_present(specs): + if subj._filename_pattern: + return True + return False ++ ++ ++def is_container(): ++ """Returns true is the system is managed as an immutable container, ++ false otherwise. If msg is True, a warning message is displayed ++ for the user. ++ """ ++ ++ bootc = '/usr/bin/bootc' ++ ostree = '/sysroot/ostree' ++ ++ if os.path.isfile(bootc) and os.access(bootc, os.X_OK): ++ p = subprocess.Popen([bootc, "status", "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) ++ (out, err) = p.communicate() ++ ++ if p.returncode == 0: ++ # check the output of 'bootc status' ++ j = json.loads(out) ++ ++ # XXX: the API from bootc status is evolving ++ status = j.get("status", "") ++ kind = j.get("kind", "") ++ ++ if kind.lower() == "bootchost" and bool(status.get("isContainer", None)): ++ return True ++ elif os.path.isdir(ostree): ++ return True ++ ++ return False +-- +2.46.2 + diff --git a/0007-Update-ostree-bootc-host-system-check.patch b/0007-Update-ostree-bootc-host-system-check.patch new file mode 100644 index 0000000..0be63d5 --- /dev/null +++ b/0007-Update-ostree-bootc-host-system-check.patch @@ -0,0 +1,106 @@ +From e2dbb97b9e13a73c47dd59827d7f2214bbdde99f Mon Sep 17 00:00:00 2001 +From: Joseph Marrero +Date: Tue, 16 Jul 2024 15:48:41 -0400 +Subject: [PATCH] Update ostree/bootc host system check. +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 6120fe52511775b60b6031d4169988c025610ab5 + +This changes the is_container() func for _is_bootc_host() +and updates the logic and message. This should detect on +all ostree and bootc hosts to date that are not using +bootc usroverlay or ostree admin unlock for development +purposes. + +Resolves: https://issues.redhat.com/browse/RHEL-49671 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 11 +++++------ + dnf/util.py | 33 ++++++++------------------------- + 2 files changed, 13 insertions(+), 31 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index c14f83639..83b190026 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -214,13 +214,12 @@ class BaseCli(dnf.Base): + elif 'test' in self.conf.tsflags: + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) +- if dnf.util.is_container(): +- _container_msg = _(""" +-*** This system is managed with ostree. Changes to the system +-*** made with dnf will be lost with the next ostree-based update. +-*** If you do not want to lose these changes, use 'rpm-ostree'. ++ if dnf.util._is_bootc_host(): ++ _bootc_host_msg = _(""" ++*** Error: system is configured to be read-only; for more ++*** information run `bootc status` or `ostree admin status`. + """) +- logger.info(_container_msg) ++ logger.info(_bootc_host_msg) + raise CliError(_("Operation aborted.")) + + if self._promptWanted(): +diff --git a/dnf/util.py b/dnf/util.py +index 1b465bda5..1ba2e27ff 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -33,13 +33,11 @@ import errno + import functools + import hawkey + import itertools +-import json + import locale + import logging + import os + import pwd + import shutil +-import subprocess + import sys + import tempfile + import time +@@ -643,30 +641,15 @@ def _is_file_pattern_present(specs): + return False + + +-def is_container(): ++def _is_bootc_host(): + """Returns true is the system is managed as an immutable container, + false otherwise. If msg is True, a warning message is displayed + for the user. + """ +- +- bootc = '/usr/bin/bootc' +- ostree = '/sysroot/ostree' +- +- if os.path.isfile(bootc) and os.access(bootc, os.X_OK): +- p = subprocess.Popen([bootc, "status", "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) +- (out, err) = p.communicate() +- +- if p.returncode == 0: +- # check the output of 'bootc status' +- j = json.loads(out) +- +- # XXX: the API from bootc status is evolving +- status = j.get("status", "") +- kind = j.get("kind", "") +- +- if kind.lower() == "bootchost" and bool(status.get("isContainer", None)): +- return True +- elif os.path.isdir(ostree): +- return True +- +- return False ++ ostree_booted = '/run/ostree-booted' ++ usr = '/usr/' ++ # Check if usr is writtable and we are in a running ostree system. ++ # We want this code to return true only when the system is in locked state. If someone ran ++ # bootc overlay or ostree admin unlock we would want normal DNF path to be ran as it will be ++ # temporary changes (until reboot). ++ return os.path.isfile(ostree_booted) and not os.access(usr, os.W_OK) +-- +2.46.2 + diff --git a/0008-Update-bootc-hosts-message-to-point-to-bootc-help.patch b/0008-Update-bootc-hosts-message-to-point-to-bootc-help.patch new file mode 100644 index 0000000..4413e74 --- /dev/null +++ b/0008-Update-bootc-hosts-message-to-point-to-bootc-help.patch @@ -0,0 +1,32 @@ +From 15aedf5f4e70695e7801c80498d4da52e49ac626 Mon Sep 17 00:00:00 2001 +From: Joseph Marrero +Date: Mon, 22 Jul 2024 15:33:32 -0400 +Subject: [PATCH] Update bootc hosts message to point to bootc --help +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: e2535589ce16bc36b96b37369502a3c312f6056a +Resolves: https://issues.redhat.com/browse/RHEL-49671 + +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 83b190026..0eda2c8cb 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -217,7 +217,7 @@ class BaseCli(dnf.Base): + if dnf.util._is_bootc_host(): + _bootc_host_msg = _(""" + *** Error: system is configured to be read-only; for more +-*** information run `bootc status` or `ostree admin status`. ++*** information run `bootc --help`. + """) + logger.info(_bootc_host_msg) + raise CliError(_("Operation aborted.")) +-- +2.46.2 + diff --git a/0009-Allow-installroot-on-read-only-bootc-system.patch b/0009-Allow-installroot-on-read-only-bootc-system.patch new file mode 100644 index 0000000..b627ec9 --- /dev/null +++ b/0009-Allow-installroot-on-read-only-bootc-system.patch @@ -0,0 +1,47 @@ +From ff86cee7cf33f44e4b10538ceeee5f284d6735ed Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= +Date: Thu, 15 Aug 2024 14:04:55 +0200 +Subject: [PATCH] Allow --installroot on read-only bootc system +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: a1aa8d0e048751859a2bec1b2fb12fcca93c6e83 + +Some people use --installroot on a read-only bootc system to install +a system into a chroot subtree. However, current bootc check did not +take into account --installroot and rejected the operation. + +This patch augments the check for the installroot being different +from /. + +It's pointless to check for installroot writability here because +installroot is written before this check when updating the +repositories and computing a transaction. Moving this check sooner +would not help because some directories (/opt, /) are kept read-only +even on writable bootc. + +Resolves: #2108 +Resolves: https://issues.redhat.com/browse/RHEL-49671 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 0eda2c8cb..008262ea0 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -214,7 +214,8 @@ class BaseCli(dnf.Base): + elif 'test' in self.conf.tsflags: + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) +- if dnf.util._is_bootc_host(): ++ if dnf.util._is_bootc_host() and \ ++ os.path.realpath(self.conf.installroot) == "/": + _bootc_host_msg = _(""" + *** Error: system is configured to be read-only; for more + *** information run `bootc --help`. +-- +2.46.2 + diff --git a/0010-Allow-downloadonly-on-read-only-bootc-system.patch b/0010-Allow-downloadonly-on-read-only-bootc-system.patch new file mode 100644 index 0000000..3f6b623 --- /dev/null +++ b/0010-Allow-downloadonly-on-read-only-bootc-system.patch @@ -0,0 +1,40 @@ +From 86bc1d60e1b8188ca5a682974d734ac3a0cdc102 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= +Date: Thu, 10 Oct 2024 10:57:48 +0200 +Subject: [PATCH] Allow --downloadonly on read-only bootc system +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 8d888d26e6da27ba37243d7504eb42472f389bde + +"dnf install --downloadonly" failed on read-only bootc system despite +not running the transaction. The downloaded packages are stored under +writable /var or to a directory explicitly choosen by a user. + +This patch suppresses the bootc read-only bailout if --downloadonly +option is used. + +https://issues.redhat.com/browse/RHEL-62028 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 008262ea0..d3844df34 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -215,7 +215,8 @@ class BaseCli(dnf.Base): + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) + if dnf.util._is_bootc_host() and \ +- os.path.realpath(self.conf.installroot) == "/": ++ os.path.realpath(self.conf.installroot) == "/" and \ ++ not self.conf.downloadonly: + _bootc_host_msg = _(""" + *** Error: system is configured to be read-only; for more + *** information run `bootc --help`. +-- +2.47.0 + diff --git a/0011-Automatic-check-availability-of-config-file.patch b/0011-Automatic-check-availability-of-config-file.patch new file mode 100644 index 0000000..b707da9 --- /dev/null +++ b/0011-Automatic-check-availability-of-config-file.patch @@ -0,0 +1,131 @@ +From 36389b63b12c2f6996772bd2bc7e98b6f3bacdf0 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Thu, 17 Oct 2024 13:30:21 +0200 +Subject: [PATCH 1/2] automatic: Check availability of config file + +Upstream commit: 13ecc3921fb1566c2af7b80d5cb515d8fc4e5ec9 + +If a configuration file is explicitly specified on the command line, +ensure that it exists and is readable. If the file is not found, notify +the user immediately and terminate the process. + +This resolves issues where users may run dnf-automatic with unrecognized +positional arguments, such as `dnf-automatic install`. + +The most natural approach to handle a non-existing config file would be +by catching the exception thrown by the `read()` method of the +`libdnf.conf.ConfigParser` class. Unfortunately, the Python bindings +override the `read()` method at the SWIG level, causing it to suppress any +potentially raised IOError. +For details see this section of the commit +https://github.com/rpm-software-management/libdnf/commit/8f1fedf8551b72d6bc24018f5980714b3a103aeb + +def ConfigParser__newRead(self, filenames): + parsedFNames = [] + try: + if isinstance(filenames, str) or isinstance(filenames, unicode): + filenames = [filenames] + except NameError: + pass + for fname in filenames: + try: + self.readFileName(fname) + parsedFNames.append(fname) + except IOError: + pass + except Exception as e: + raise RuntimeError("Parsing file '%s' failed: %s" % (fname, str(e))) + return parsedFNames +ConfigParser.read = ConfigParser__newRead + +Resolves: https://issues.redhat.com/browse/RHEL-68979 +--- + dnf/automatic/main.py | 19 +++++++++++++++---- + 1 file changed, 15 insertions(+), 4 deletions(-) + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index 243e3015..21aa82b7 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -74,7 +74,7 @@ def build_emitters(conf): + + def parse_arguments(args): + parser = argparse.ArgumentParser() +- parser.add_argument('conf_path', nargs='?', default=dnf.const.CONF_AUTOMATIC_FILENAME) ++ parser.add_argument('conf_path', nargs='?') + parser.add_argument('--timer', action='store_true') + parser.add_argument('--installupdates', dest='installupdates', action='store_true') + parser.add_argument('--downloadupdates', dest='downloadupdates', action='store_true') +@@ -89,7 +89,17 @@ def parse_arguments(args): + class AutomaticConfig(object): + def __init__(self, filename=None, downloadupdates=None, + installupdates=None): +- if not filename: ++ if filename: ++ # Specific config file was explicitely requested. Check that it exists ++ # and is readable. ++ if os.access(filename, os.F_OK): ++ if not os.access(filename, os.R_OK): ++ raise dnf.exceptions.Error( ++ "Configuration file \"{}\" is not readable.".format(filename)) ++ else: ++ raise dnf.exceptions.Error( ++ "Configuration file \"{}\" not found.".format(filename)) ++ else: + filename = dnf.const.CONF_AUTOMATIC_FILENAME + self.commands = CommandsConfig() + self.email = EmailConfig() +@@ -302,11 +312,12 @@ def wait_for_network(repos, timeout): + + def main(args): + (opts, parser) = parse_arguments(args) ++ conf = None ++ emitters = None + + try: + conf = AutomaticConfig(opts.conf_path, opts.downloadupdates, + opts.installupdates) +- emitters = None + with dnf.Base() as base: + cli = dnf.cli.Cli(base) + cli._read_conf_file() +@@ -371,7 +382,7 @@ def main(args): + raise dnf.exceptions.Error('reboot command returned nonzero exit code: %d', exit_code) + except dnf.exceptions.Error as exc: + logger.error(_('Error: %s'), ucd(exc)) +- if conf.emitters.send_error_messages and emitters != None: ++ if conf is not None and conf.emitters.send_error_messages and emitters is not None: + emitters.notify_error(_('Error: %s') % str(exc)) + emitters.commit() + return 1 +-- +2.47.1 + + +From bfc4d7ae2d9a567aaf656388c8b914b7a03db39d Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Thu, 12 Dec 2024 08:09:48 +0100 +Subject: [PATCH 2/2] automatic: Fix incorrect Error class instantiation + +dnf.exceptions.Error class constructor accepts only one argument - error +message. +--- + dnf/automatic/main.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index 21aa82b7..1a1e1cf0 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -379,7 +379,7 @@ def main(args): + (conf.commands.reboot == 'when-needed' and base.reboot_needed())): + exit_code = os.waitstatus_to_exitcode(os.system(conf.commands.reboot_command)) + if exit_code != 0: +- raise dnf.exceptions.Error('reboot command returned nonzero exit code: %d', exit_code) ++ raise dnf.exceptions.Error('reboot command returned nonzero exit code: %d' % exit_code) + except dnf.exceptions.Error as exc: + logger.error(_('Error: %s'), ucd(exc)) + if conf is not None and conf.emitters.send_error_messages and emitters is not None: +-- +2.47.1 + diff --git a/0012-Add-support-for-transient.patch b/0012-Add-support-for-transient.patch new file mode 100644 index 0000000..298cee8 --- /dev/null +++ b/0012-Add-support-for-transient.patch @@ -0,0 +1,181 @@ +From 082973b36646945b1c60be8b96ab628d92d99b92 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 7 Nov 2024 02:31:25 +0000 +Subject: [PATCH 12/17] Add support for --transient +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 6091f3fccea988208ca417a504569947e4c99263 + +Adds support for the --transient option on all transactions. Passing +--transient on a bootc system will call `bootc usr-overlay` to create a +transient writeable /usr and continue the transaction. + +Specifying --transient on a non-bootc system will throw an error; we +don't want to mislead users to thinking this feature works on non-bootc +systems. + +If --transient is not specified and the bootc system is in a locked +state, the operation will be aborted and a message will be printed +suggesting to try again with --transient. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 40 ++++++++++++++++++++++++++++++--------- + dnf/cli/option_parser.py | 3 +++ + dnf/conf/config.py | 2 +- + dnf/util.py | 41 +++++++++++++++++++++++++++++----------- + 4 files changed, 65 insertions(+), 21 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index d3844df34..53f2d9d50 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -205,28 +205,50 @@ class BaseCli(dnf.Base): + else: + self.output.reportDownloadSize(install_pkgs, install_only) + ++ bootc_unlock_requested = False ++ + if trans or self._moduleContainer.isChanged() or \ + (self._history and (self._history.group or self._history.env)): + # confirm with user + if self.conf.downloadonly: + logger.info(_("{prog} will only download packages for the transaction.").format( + prog=dnf.util.MAIN_PROG_UPPER)) ++ + elif 'test' in self.conf.tsflags: + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) +- if dnf.util._is_bootc_host() and \ +- os.path.realpath(self.conf.installroot) == "/" and \ +- not self.conf.downloadonly: +- _bootc_host_msg = _(""" +-*** Error: system is configured to be read-only; for more +-*** information run `bootc --help`. +-""") +- logger.info(_bootc_host_msg) +- raise CliError(_("Operation aborted.")) ++ ++ is_bootc_transaction = dnf.util._is_bootc_host() and \ ++ os.path.realpath(self.conf.installroot) == "/" and \ ++ not self.conf.downloadonly ++ ++ # Handle bootc transactions. `--transient` must be specified if ++ # /usr is not already writeable. ++ if is_bootc_transaction: ++ if self.conf.persistence == "persist": ++ logger.info(_("Persistent transactions aren't supported on bootc systems.")) ++ raise CliError(_("Operation aborted.")) ++ assert self.conf.persistence in ("auto", "transient") ++ if not dnf.util._is_bootc_unlocked(): ++ if self.conf.persistence == "auto": ++ logger.info(_("This bootc system is configured to be read-only. Pass --transient to " ++ "perform this and subsequent transactions in a transient overlay which " ++ "will reset when the system reboots.")) ++ raise CliError(_("Operation aborted.")) ++ assert self.conf.persistence == "transient" ++ logger.info(_("A transient overlay will be created on /usr that will be discarded on reboot. " ++ "Keep in mind that changes to /etc and /var will still persist, and packages " ++ "commonly modify these directories.")) ++ bootc_unlock_requested = True ++ elif self.conf.persistence == "transient": ++ raise CliError(_("Transient transactions are only supported on bootc systems.")) + + if self._promptWanted(): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) ++ ++ if bootc_unlock_requested: ++ dnf.util._bootc_unlock() + else: + logger.info(_('Nothing to do.')) + return +diff --git a/dnf/cli/option_parser.py b/dnf/cli/option_parser.py +index 042d5fbbe..ec4696fd2 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -317,6 +317,9 @@ class OptionParser(argparse.ArgumentParser): + general_grp.add_argument("--downloadonly", dest="downloadonly", + action="store_true", default=False, + help=_("only download packages")) ++ general_grp.add_argument("--transient", dest="persistence", ++ action="store_const", const="transient", default=None, ++ help=_("Use a transient overlay which will reset on reboot")) + general_grp.add_argument("--comment", dest="comment", default=None, + help=_("add a comment to transaction")) + # Updateinfo options... +diff --git a/dnf/conf/config.py b/dnf/conf/config.py +index f9c8d932a..5210ffba2 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -343,7 +343,7 @@ class MainConf(BaseConfig): + 'best', 'assumeyes', 'assumeno', 'clean_requirements_on_remove', 'gpgcheck', + 'showdupesfromrepos', 'plugins', 'ip_resolve', + 'rpmverbosity', 'disable_excludes', 'color', +- 'downloadonly', 'exclude', 'excludepkgs', 'skip_broken', ++ 'downloadonly', 'persistence', 'exclude', 'excludepkgs', 'skip_broken', + 'tsflags', 'arch', 'basearch', 'ignorearch', 'cacheonly', 'comment'] + + for name in config_args: +diff --git a/dnf/util.py b/dnf/util.py +index 1ba2e27ff..2e270890c 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -38,6 +38,7 @@ import logging + import os + import pwd + import shutil ++import subprocess + import sys + import tempfile + import time +@@ -642,14 +643,32 @@ def _is_file_pattern_present(specs): + + + def _is_bootc_host(): +- """Returns true is the system is managed as an immutable container, +- false otherwise. If msg is True, a warning message is displayed +- for the user. +- """ +- ostree_booted = '/run/ostree-booted' +- usr = '/usr/' +- # Check if usr is writtable and we are in a running ostree system. +- # We want this code to return true only when the system is in locked state. If someone ran +- # bootc overlay or ostree admin unlock we would want normal DNF path to be ran as it will be +- # temporary changes (until reboot). +- return os.path.isfile(ostree_booted) and not os.access(usr, os.W_OK) ++ """Returns true is the system is managed as an immutable container, false ++ otherwise.""" ++ ostree_booted = "/run/ostree-booted" ++ return os.path.isfile(ostree_booted) ++ ++ ++def _is_bootc_unlocked(): ++ """Check whether /usr is writeable, e.g. if we are in a normal mutable ++ system or if we are in a bootc after `bootc usr-overlay` or `ostree admin ++ unlock` was run.""" ++ usr = "/usr" ++ return os.access(usr, os.W_OK) ++ ++ ++def _bootc_unlock(): ++ """Set up a writeable overlay on bootc systems.""" ++ ++ if _is_bootc_unlocked(): ++ return ++ ++ unlock_command = ["bootc", "usr-overlay"] ++ ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("bootc command not found. Is this a bootc system?")) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) +-- +2.48.1 + diff --git a/0013-bootc-Document-transient-and-persistence.patch b/0013-bootc-Document-transient-and-persistence.patch new file mode 100644 index 0000000..1797892 --- /dev/null +++ b/0013-bootc-Document-transient-and-persistence.patch @@ -0,0 +1,74 @@ +From 83177634e2a50887d9910048f87277b838eaa2f2 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 17 Dec 2024 18:58:32 +0000 +Subject: [PATCH 13/17] bootc: Document `--transient` and `persistence` +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 80a62d89ba3c00f4e0bd3fea995d04ccf9ba8098 + +Documents the new `--transient` command-line argument and `persistence` +configuration option. I tried to use a table for listing the valid +options for `persistence`, but RST does not automatically wrap table +cells containing long lines, so a list was much easier. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + doc/command_ref.rst | 9 +++++++++ + doc/conf_ref.rst | 11 +++++++++++ + 2 files changed, 20 insertions(+) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 5684b0611..2337a2e29 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -388,6 +388,11 @@ Options + ``--showduplicates`` + Show duplicate packages in repositories. Applicable for the list and search commands. + ++.. _transient_option-label: ++ ++``--transient`` ++ Applicable only on bootc (bootable containers) systems. Perform transactions using a transient overlay which will be lost on the next reboot. See also the :ref:`persistence ` configuration option. ++ + .. _verbose_options-label: + + ``-v, --verbose`` +@@ -707,6 +712,10 @@ transactions and act according to this information (assuming the + which specifies a transaction by a package which it manipulated. When no + transaction is specified, list all known transactions. + ++ Note that transient transactions (see :ref:`--transient ++ `) will be listed even though they do not make ++ persistent changes to files under ``/usr`` or to the RPM database. ++ + The "Action(s)" column lists each type of action taken in the transaction. The possible values are: + + * Install (I): a new package was installed on the system +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index 240b35f96..d4e4a2770 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -442,6 +442,17 @@ configuration file by your distribution to override the DNF defaults. + + Directory where DNF stores its persistent data between runs. Default is ``"/var/lib/dnf"``. + ++.. _persistence-label: ++ ++``persistence`` ++ :ref:`string ` ++ ++ Whether changes should persist across system reboots. Default is ``auto``. Passing :ref:`--transient ` will override this setting to ``transient``. Valid values are: ++ ++ * ``auto``: Changes will persist across reboots, unless the target is a running bootc system and the system is already in an unlocked state (i.e. ``/usr`` is writable). ++ * ``transient``: Changes will be lost on the next reboot. Only applicable on bootc systems. Beware that changes to ``/etc`` and ``/var`` will persist, depending on the configuration of your bootc system. See also https://containers.github.io/bootc/man/bootc-usr-overlay.html. ++ * ``persist``: Changes will persist across reboots. ++ + .. _pluginconfpath-label: + + ``pluginconfpath`` +-- +2.48.1 + diff --git a/0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch b/0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch new file mode 100644 index 0000000..52ab372 --- /dev/null +++ b/0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch @@ -0,0 +1,166 @@ +From c0f2329c2ed71f373aad26c7f1786494f6e75b76 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 15 Jan 2025 21:43:58 +0000 +Subject: [PATCH 14/17] bootc: Use ostree GObject API to get deployment status +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: f3abee56452e40ce475f714666c3a16426759c96 + +Using libostree gives us more detail about the current state of the +deployment than only checking whether /usr is writable. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 13 ++++---- + dnf/util.py | 80 +++++++++++++++++++++++++++++++++++--------------- + 2 files changed, 65 insertions(+), 28 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 53f2d9d50..bc11505fc 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -218,18 +218,22 @@ class BaseCli(dnf.Base): + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) + +- is_bootc_transaction = dnf.util._is_bootc_host() and \ ++ is_bootc_transaction = dnf.util._Bootc.is_bootc_host() and \ + os.path.realpath(self.conf.installroot) == "/" and \ + not self.conf.downloadonly + + # Handle bootc transactions. `--transient` must be specified if + # /usr is not already writeable. ++ bootc = None + if is_bootc_transaction: + if self.conf.persistence == "persist": + logger.info(_("Persistent transactions aren't supported on bootc systems.")) + raise CliError(_("Operation aborted.")) + assert self.conf.persistence in ("auto", "transient") +- if not dnf.util._is_bootc_unlocked(): ++ ++ bootc = dnf.util._Bootc() ++ ++ if not bootc.is_unlocked(): + if self.conf.persistence == "auto": + logger.info(_("This bootc system is configured to be read-only. Pass --transient to " + "perform this and subsequent transactions in a transient overlay which " +@@ -239,7 +243,6 @@ class BaseCli(dnf.Base): + logger.info(_("A transient overlay will be created on /usr that will be discarded on reboot. " + "Keep in mind that changes to /etc and /var will still persist, and packages " + "commonly modify these directories.")) +- bootc_unlock_requested = True + elif self.conf.persistence == "transient": + raise CliError(_("Transient transactions are only supported on bootc systems.")) + +@@ -247,8 +250,8 @@ class BaseCli(dnf.Base): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) + +- if bootc_unlock_requested: +- dnf.util._bootc_unlock() ++ if bootc: ++ bootc.unlock_and_prepare() + else: + logger.info(_('Nothing to do.')) + return +diff --git a/dnf/util.py b/dnf/util.py +index 2e270890c..51f853d8b 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -642,33 +642,67 @@ def _is_file_pattern_present(specs): + return False + + +-def _is_bootc_host(): +- """Returns true is the system is managed as an immutable container, false +- otherwise.""" +- ostree_booted = "/run/ostree-booted" +- return os.path.isfile(ostree_booted) ++class _Bootc: ++ usr = "/usr" + ++ def __init__(self): ++ if not self.is_bootc_host(): ++ raise RuntimeError(_("Not running on a bootc system.")) + +-def _is_bootc_unlocked(): +- """Check whether /usr is writeable, e.g. if we are in a normal mutable +- system or if we are in a bootc after `bootc usr-overlay` or `ostree admin +- unlock` was run.""" +- usr = "/usr" +- return os.access(usr, os.W_OK) ++ import gi ++ self._gi = gi + ++ gi.require_version("OSTree", "1.0") ++ from gi.repository import OSTree + +-def _bootc_unlock(): +- """Set up a writeable overlay on bootc systems.""" ++ self._OSTree = OSTree + +- if _is_bootc_unlocked(): +- return ++ self._sysroot = self._OSTree.Sysroot.new_default() ++ assert self._sysroot.load(None) + +- unlock_command = ["bootc", "usr-overlay"] ++ self._booted_deployment = self._sysroot.require_booted_deployment() ++ assert self._booted_deployment is not None + +- try: +- completed_process = subprocess.run(unlock_command, text=True) +- completed_process.check_returncode() +- except FileNotFoundError: +- raise dnf.exceptions.Error(_("bootc command not found. Is this a bootc system?")) +- except subprocess.CalledProcessError: +- raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ @staticmethod ++ def is_bootc_host(): ++ """Returns true is the system is managed as an immutable container, false ++ otherwise.""" ++ ostree_booted = "/run/ostree-booted" ++ return os.path.isfile(ostree_booted) ++ ++ def _get_unlocked_state(self): ++ return self._booted_deployment.get_unlocked() ++ ++ def is_unlocked(self): ++ return self._get_unlocked_state() != self._OSTree.DeploymentUnlockedState.NONE ++ ++ def unlock_and_prepare(self): ++ """Set up a writeable overlay on bootc systems.""" ++ ++ bootc_unlocked_state = self._get_unlocked_state() ++ ++ valid_bootc_unlocked_states = ( ++ self._OSTree.DeploymentUnlockedState.NONE, ++ self._OSTree.DeploymentUnlockedState.DEVELOPMENT, ++ # self._OSTree.DeploymentUnlockedState.TRANSIENT, ++ # self._OSTree.DeploymentUnlockedState.HOTFIX, ++ ) ++ ++ if bootc_unlocked_state not in valid_bootc_unlocked_states: ++ raise ValueError(_("Unhandled bootc unlocked state: %s") % bootc_unlocked_state.value_nick) ++ ++ if bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.DEVELOPMENT: ++ # System is already unlocked. ++ pass ++ elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.NONE: ++ unlock_command = ["ostree", "admin", "unlock"] ++ ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("ostree command not found. Is this a bootc system?")) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ ++ assert os.access(self.usr, os.W_OK) +-- +2.48.1 + diff --git a/0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch b/0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch new file mode 100644 index 0000000..a3975df --- /dev/null +++ b/0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch @@ -0,0 +1,237 @@ +From 47cd04de2c099171002f2f474084e08eb5c7dddd Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 16 Jan 2025 14:06:26 -0500 +Subject: [PATCH 15/17] bootc: "Re-locking": use ostree admin unlock + --transient +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: fa47a256ae7add2ce1c99ae8bedce7216001f396 + +To keep /usr read-only after DNF is finished with a transient +transaction, we call `ostree admin unlock --transient` to mount the /usr +overlay as read-only by default. Then, we create a private mount +namespace for DNF and its child processes and remount the /usr overlayfs +as read/write in the private mountns. + +os.unshare is unfortunately only available in Python >= 3.12, so we have +to call libc.unshare via Python ctypes here and hardcode the CLONE_NEWNS +flag that we need to pass. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 33 ++++++++++--------- + dnf/util.py | 86 ++++++++++++++++++++++++++++++++++++++------------ + 2 files changed, 83 insertions(+), 36 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index bc11505fc..e2700c092 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -205,8 +205,6 @@ class BaseCli(dnf.Base): + else: + self.output.reportDownloadSize(install_pkgs, install_only) + +- bootc_unlock_requested = False +- + if trans or self._moduleContainer.isChanged() or \ + (self._history and (self._history.group or self._history.env)): + # confirm with user +@@ -218,40 +216,45 @@ class BaseCli(dnf.Base): + logger.info(_("{prog} will only download packages, install gpg keys, and check the " + "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) + +- is_bootc_transaction = dnf.util._Bootc.is_bootc_host() and \ ++ is_bootc_transaction = dnf.util._BootcSystem.is_bootc_system() and \ + os.path.realpath(self.conf.installroot) == "/" and \ + not self.conf.downloadonly + + # Handle bootc transactions. `--transient` must be specified if + # /usr is not already writeable. +- bootc = None ++ bootc_system = None + if is_bootc_transaction: + if self.conf.persistence == "persist": + logger.info(_("Persistent transactions aren't supported on bootc systems.")) + raise CliError(_("Operation aborted.")) + assert self.conf.persistence in ("auto", "transient") + +- bootc = dnf.util._Bootc() ++ bootc_system = dnf.util._BootcSystem() + +- if not bootc.is_unlocked(): ++ if not bootc_system.is_writable(): + if self.conf.persistence == "auto": + logger.info(_("This bootc system is configured to be read-only. Pass --transient to " +- "perform this and subsequent transactions in a transient overlay which " +- "will reset when the system reboots.")) ++ "perform this transaction in a transient overlay which will reset when " ++ "the system reboots.")) + raise CliError(_("Operation aborted.")) + assert self.conf.persistence == "transient" +- logger.info(_("A transient overlay will be created on /usr that will be discarded on reboot. " +- "Keep in mind that changes to /etc and /var will still persist, and packages " +- "commonly modify these directories.")) +- elif self.conf.persistence == "transient": +- raise CliError(_("Transient transactions are only supported on bootc systems.")) ++ if not bootc_system.is_unlocked_transient(): ++ # Only tell the user about the transient overlay if ++ # it's not already in place ++ logger.info(_("A transient overlay will be created on /usr that will be discarded on reboot. " ++ "Keep in mind that changes to /etc and /var will still persist, and packages " ++ "commonly modify these directories.")) ++ else: ++ # Not a bootc transaction. ++ if self.conf.persistence == "transient": ++ raise CliError(_("Transient transactions are only supported on bootc systems.")) + + if self._promptWanted(): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) + +- if bootc: +- bootc.unlock_and_prepare() ++ if bootc_system: ++ bootc_system.make_writable() + else: + logger.info(_('Nothing to do.')) + return +diff --git a/dnf/util.py b/dnf/util.py +index 51f853d8b..eb987bb8a 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -25,6 +25,7 @@ from __future__ import unicode_literals + from .pycomp import PY3, basestring + from dnf.i18n import _, ucd + import argparse ++import ctypes + import dnf + import dnf.callback + import dnf.const +@@ -642,11 +643,12 @@ def _is_file_pattern_present(specs): + return False + + +-class _Bootc: ++class _BootcSystem: + usr = "/usr" ++ CLONE_NEWNS = 0x00020000 # defined in linux/include/uapi/linux/sched.h + + def __init__(self): +- if not self.is_bootc_host(): ++ if not self.is_bootc_system(): + raise RuntimeError(_("Not running on a bootc system.")) + + import gi +@@ -664,45 +666,87 @@ class _Bootc: + assert self._booted_deployment is not None + + @staticmethod +- def is_bootc_host(): ++ def is_bootc_system(): + """Returns true is the system is managed as an immutable container, false + otherwise.""" + ostree_booted = "/run/ostree-booted" + return os.path.isfile(ostree_booted) + ++ @classmethod ++ def is_writable(cls): ++ """Returns true if and only if /usr is writable.""" ++ return os.access(cls.usr, os.W_OK) ++ + def _get_unlocked_state(self): + return self._booted_deployment.get_unlocked() + +- def is_unlocked(self): +- return self._get_unlocked_state() != self._OSTree.DeploymentUnlockedState.NONE ++ def is_unlocked_transient(self): ++ """Returns true if and only if the bootc system is unlocked in a ++ transient state, i.e. a overlayfs is mounted as read-only on /usr. ++ Changes can be made to the overlayfs by remounting /usr as ++ read/write in a private mount namespace.""" ++ return self._get_unlocked_state() == self._OSTree.DeploymentUnlockedState.TRANSIENT ++ ++ @classmethod ++ def _set_up_mountns(cls): ++ # os.unshare is only available in Python >= 3.12. ++ ++ # Access symbols in libraries loaded by the Python interpreter, ++ # which will include libc. See https://bugs.python.org/issue34592. ++ libc = ctypes.CDLL(None) ++ if libc.unshare(cls.CLONE_NEWNS) != 0: ++ raise OSError("Failed to unshare mount namespace") ++ ++ mount_command = ["mount", "--options-source=disable", "-o", "remount,rw", cls.usr] ++ try: ++ completed_process = subprocess.run(mount_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("%s: command not found.") % mount_command[0]) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to mount %s as read/write: %s", cls.usr, completed_process.stderr)) + +- def unlock_and_prepare(self): +- """Set up a writeable overlay on bootc systems.""" ++ @staticmethod ++ def _unlock(): ++ unlock_command = ["ostree", "admin", "unlock", "--transient"] ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("%s: command not found. Is this a bootc system?") % unlock_command[0]) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ ++ def make_writable(self): ++ """Set up a writable overlay on bootc systems.""" + + bootc_unlocked_state = self._get_unlocked_state() + + valid_bootc_unlocked_states = ( + self._OSTree.DeploymentUnlockedState.NONE, + self._OSTree.DeploymentUnlockedState.DEVELOPMENT, +- # self._OSTree.DeploymentUnlockedState.TRANSIENT, +- # self._OSTree.DeploymentUnlockedState.HOTFIX, ++ self._OSTree.DeploymentUnlockedState.TRANSIENT, ++ self._OSTree.DeploymentUnlockedState.HOTFIX, + ) +- + if bootc_unlocked_state not in valid_bootc_unlocked_states: + raise ValueError(_("Unhandled bootc unlocked state: %s") % bootc_unlocked_state.value_nick) + +- if bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.DEVELOPMENT: +- # System is already unlocked. ++ writable_unlocked_states = ( ++ self._OSTree.DeploymentUnlockedState.DEVELOPMENT, ++ self._OSTree.DeploymentUnlockedState.HOTFIX, ++ ) ++ if bootc_unlocked_state in writable_unlocked_states: ++ # System is already unlocked in development mode, and usr is ++ # already mounted read/write. + pass + elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.NONE: +- unlock_command = ["ostree", "admin", "unlock"] +- +- try: +- completed_process = subprocess.run(unlock_command, text=True) +- completed_process.check_returncode() +- except FileNotFoundError: +- raise dnf.exceptions.Error(_("ostree command not found. Is this a bootc system?")) +- except subprocess.CalledProcessError: +- raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ # System is not unlocked. Unlock it in transient mode, then set up ++ # a mount namespace for DNF. ++ self._unlock() ++ self._set_up_mountns() ++ elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.TRANSIENT: ++ # System is unlocked in transient mode, so usr is mounted ++ # read-only. Set up a mount namespace for DNF. ++ self._set_up_mountns() + + assert os.access(self.usr, os.W_OK) +-- +2.48.1 + diff --git a/0016-spec-Add-dnf-bootc-subpackage.patch b/0016-spec-Add-dnf-bootc-subpackage.patch new file mode 100644 index 0000000..39795c2 --- /dev/null +++ b/0016-spec-Add-dnf-bootc-subpackage.patch @@ -0,0 +1,56 @@ +From dc461487b3a1323d96183e5c229a307d274a3ecc Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 28 Jan 2025 11:27:00 -0500 +Subject: [PATCH 16/17] spec: Add dnf-bootc subpackage +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 76a0c339eb172b1b2a4b1aa8b4db8d6a5145916b + +dnf-bootc's only job is to Require python3-gobject-base, ostree, +ostree-libs, and util-linux-core, which are needed to interact with +bootc systems. We don't want to add these dependencies on `python3-dnf` +because we don't want them on non-bootc systems, so we use a subpackage. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf.spec | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/dnf.spec b/dnf.spec +index 98b6be42c..c749ab41e 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -191,6 +191,17 @@ Requires: python3-%{name} = %{version}-%{release} + %description automatic + Systemd units that can periodically download package upgrades and apply them. + ++%package bootc ++Summary: %{pkg_summary} - additional bootc dependencies ++Requires: python3-%{name} = %{version}-%{release} ++Requires: ostree ++Requires: ostree-libs ++Requires: python3-gobject-base ++Requires: util-linux-core ++ ++%description bootc ++Additional dependencies needed to perform transactions on booted bootc (bootable containers) systems. ++ + + %prep + %autosetup +@@ -413,6 +424,9 @@ popd + %{_unitdir}/%{name}-automatic-install.timer + %{python3_sitelib}/%{name}/automatic/ + ++%files bootc ++# bootc subpackage does not include any files ++ + %changelog + * Wed Apr 24 2024 Jan Kolarik - 4.20.0-1 + - repoquery: Fix loading filelists when -f is used (RhBug:2276012) +-- +2.48.1 + diff --git a/0017-Require-libdnf-0.74.0-with-persistence-option.patch b/0017-Require-libdnf-0.74.0-with-persistence-option.patch new file mode 100644 index 0000000..bf59dca --- /dev/null +++ b/0017-Require-libdnf-0.74.0-with-persistence-option.patch @@ -0,0 +1,45 @@ +From 7ffd97532e120f4391792b1bdfa0dbe1510409a7 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 5 Feb 2025 10:35:08 -0500 +Subject: [PATCH 17/17] Require libdnf >= 0.74.0 with `persistence` option +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 5a4f6c42e61ed764ff85eea69125947a280d665d + +This backport actually uses RHEL 10 version of libdnf. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf.spec | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/dnf.spec b/dnf.spec +index c749ab41e..66243c858 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -2,7 +2,7 @@ + %define __cmake_in_source_build 1 + + # default dependencies +-%global hawkey_version 0.73.1 ++%global hawkey_version 0.74.0 + %global libcomps_version 0.1.8 + %global libmodulemd_version 2.9.3 + %global rpm_version 4.14.0 +@@ -23,6 +23,10 @@ + %global rpm_version 4.11.3-25.el7.centos.1 + %endif + ++%if 0%{?rhel} == 10 ++ %global hawkey_version 0.73.1-7 ++%endif ++ + # override dependencies for fedora 26 + %if 0%{?fedora} == 26 + %global rpm_version 4.13.0.1-7 +-- +2.48.1 + diff --git a/0018-Derive-releasever_-major-minor-in-conf-not-substitut.patch b/0018-Derive-releasever_-major-minor-in-conf-not-substitut.patch new file mode 100644 index 0000000..9ed8350 --- /dev/null +++ b/0018-Derive-releasever_-major-minor-in-conf-not-substitut.patch @@ -0,0 +1,162 @@ +From ad24340a6ccf06a4c948824f394a0de5b6a7826d Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 20 Jan 2025 21:36:18 +0000 +Subject: [PATCH] Derive releasever_{major,minor} in conf, not substitutions + +Upstream commit: 0e283fb3b0b06f1d31c6d3bd9c14c5fa89bf64a4 + +This allows setting a releasever_major or releasever_minor +independent of releasever, which is needed by EPEL. + +Related: https://issues.redhat.com/browse/RHEL-68034 +--- + dnf/conf/config.py | 26 ++++++++++++++++++++++++++ + dnf/conf/substitutions.py | 17 +++-------------- + tests/conf/test_substitutions.py | 19 +++++++++---------- + tests/test_config.py | 16 ++++++++++++++++ + 4 files changed, 54 insertions(+), 24 deletions(-) + +diff --git a/dnf/conf/config.py b/dnf/conf/config.py +index 5210ffba..6cf28724 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -430,6 +430,32 @@ class MainConf(BaseConfig): + return + self.substitutions['releasever'] = str(val) + ++ @property ++ def releasever_major(self): ++ # :api ++ return self.substitutions.get('releasever_major') ++ ++ @releasever_major.setter ++ def releasever_major(self, val): ++ # :api ++ if val is None: ++ self.substitutions.pop('releasever_major', None) ++ return ++ self.substitutions['releasever_major'] = str(val) ++ ++ @property ++ def releasever_minor(self): ++ # :api ++ return self.substitutions.get('releasever_minor') ++ ++ @releasever_minor.setter ++ def releasever_minor(self, val): ++ # :api ++ if val is None: ++ self.substitutions.pop('releasever_minor', None) ++ return ++ self.substitutions['releasever_minor'] = str(val) ++ + @property + def arch(self): + # :api +diff --git a/dnf/conf/substitutions.py b/dnf/conf/substitutions.py +index 5c736a8d..8582d5d8 100644 +--- a/dnf/conf/substitutions.py ++++ b/dnf/conf/substitutions.py +@@ -22,11 +22,12 @@ import logging + import os + import re + ++from libdnf.conf import ConfigParser + from dnf.i18n import _ + from dnf.exceptions import ReadOnlyVariableError + + ENVIRONMENT_VARS_RE = re.compile(r'^DNF_VAR_[A-Za-z0-9_]+$') +-READ_ONLY_VARIABLES = frozenset(("releasever_major", "releasever_minor")) ++READ_ONLY_VARIABLES = frozenset() + logger = logging.getLogger('dnf') + + +@@ -45,18 +46,6 @@ class Substitutions(dict): + elif key in numericvars: + self[key] = val + +- @staticmethod +- def _split_releasever(releasever): +- # type: (str) -> tuple[str, str] +- pos = releasever.find(".") +- if pos == -1: +- releasever_major = releasever +- releasever_minor = "" +- else: +- releasever_major = releasever[:pos] +- releasever_minor = releasever[pos + 1:] +- return releasever_major, releasever_minor +- + def __setitem__(self, key, value): + if Substitutions.is_read_only(key): + raise ReadOnlyVariableError(f"Variable \"{key}\" is read-only", variable_name=key) +@@ -65,7 +54,7 @@ class Substitutions(dict): + setitem(key, value) + + if key == "releasever" and value: +- releasever_major, releasever_minor = Substitutions._split_releasever(value) ++ releasever_major, releasever_minor = ConfigParser.splitReleasever(value) + setitem("releasever_major", releasever_major) + setitem("releasever_minor", releasever_minor) + +diff --git a/tests/conf/test_substitutions.py b/tests/conf/test_substitutions.py +index d8ac3c20..78d3e727 100644 +--- a/tests/conf/test_substitutions.py ++++ b/tests/conf/test_substitutions.py +@@ -56,16 +56,6 @@ class SubstitutionsFromEnvironmentTest(tests.support.TestCase): + self.assertEqual('opera', conf.substitutions['GENRE']) + + +-class SubstitutionsReadOnlyTest(tests.support.TestCase): +- def test_set_readonly(self): +- conf = dnf.conf.Conf() +- variable_name = "releasever_major" +- self.assertTrue(Substitutions.is_read_only(variable_name)) +- with self.assertRaises(ReadOnlyVariableError) as cm: +- conf.substitutions["releasever_major"] = "1" +- self.assertEqual(cm.exception.variable_name, "releasever_major") +- +- + class SubstitutionsReleaseverTest(tests.support.TestCase): + def test_releasever_simple(self): + conf = dnf.conf.Conf() +@@ -84,3 +74,12 @@ class SubstitutionsReleaseverTest(tests.support.TestCase): + conf.substitutions["releasever"] = "1.23.45" + self.assertEqual(conf.substitutions["releasever_major"], "1") + self.assertEqual(conf.substitutions["releasever_minor"], "23.45") ++ ++ def test_releasever_major_minor_overrides(self): ++ conf = dnf.conf.Conf() ++ conf.substitutions["releasever"] = "1.23" ++ conf.substitutions["releasever_major"] = "45" ++ conf.substitutions["releasever_minor"] = "67" ++ self.assertEqual(conf.substitutions["releasever"], "1.23") ++ self.assertEqual(conf.substitutions["releasever_major"], "45") ++ self.assertEqual(conf.substitutions["releasever_minor"], "67") +diff --git a/tests/test_config.py b/tests/test_config.py +index d8502670..16bdcccb 100644 +--- a/tests/test_config.py ++++ b/tests/test_config.py +@@ -145,3 +145,19 @@ class ConfTest(tests.support.TestCase): + conf = Conf() + with self.assertRaises(dnf.exceptions.ConfigError): + conf.debuglevel = '11' ++ ++ def test_releasever_major_minor(self): ++ conf = Conf() ++ conf.releasever = '1.2' ++ self.assertEqual(conf.releasever_major, '1') ++ self.assertEqual(conf.releasever_minor, '2') ++ ++ # override releasever_major ++ conf.releasever_major = '3' ++ self.assertEqual(conf.releasever_major, '3') ++ self.assertEqual(conf.releasever_minor, '2') ++ ++ # override releasever_minor ++ conf.releasever_minor = '4' ++ self.assertEqual(conf.releasever_major, '3') ++ self.assertEqual(conf.releasever_minor, '4') +-- +2.48.1 + diff --git a/0019-Override-releasever_-major-minor-with-provides.patch b/0019-Override-releasever_-major-minor-with-provides.patch new file mode 100644 index 0000000..142ff1b --- /dev/null +++ b/0019-Override-releasever_-major-minor-with-provides.patch @@ -0,0 +1,179 @@ +From 66fb0b5ec8ea3d3165e6b2f38e1a01f692d1ec93 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 21 Jan 2025 19:16:13 +0000 +Subject: [PATCH] Override releasever_{major,minor} with provides + +Upstream commit: 75e3ff0c43a5d605aa43d6ddc2d9e2ef89942999 + +The releasever_major and releasever_minor substitution variables are +usually derived by splitting releasever on the first `.`. However, to +support EPEL 10 [1], we would like a way for distributions to override these +values. Specifically, we would like RHEL 10 to have a releasever of `10` +with a releasever_major of `10` and a releasever_minor of `0` (later +incrementing to `1`, `2`, to correspond with the RHEL minor version). + +This commit adds a new API function, `detect_releasevers`, which derives +releasever, releasever_major, and releasever_minor from virtual provides +on the system-release package (any of `DISTROVERPKG`). The detection of +releasever is unchanged. releasever_major and releasever_minor are +specified by the versions of the `system-release-major` and +`system-release-minor` provides, respectively. + +If the user specifies a `--releasever=X.Y` on the command line, the +distribution settings for releasever, releasever_major, and releasever_minor +will all be overridden: releasever will be set to X.Y, releasever_major will be +set to X, and releasever_minor will be set to Y, same as before. If a user +wants to specify a custom releasever_major and releasever_minor, they have to +set all three with `--setopt=releasever=X --setopt=releasever_major=Y +--setopt=releasever_minor=z`, taking care to put `releasever_major` and +`releasever_minor` after `releasever` so they are not overridden. + +[1] https://issues.redhat.com/browse/RHEL-68034 +--- + dnf/base.py | 10 ++++++++-- + dnf/cli/cli.py | 11 +++++++++-- + dnf/const.py.in | 2 ++ + dnf/rpm/__init__.py | 43 +++++++++++++++++++++++++++++++++++++++---- + 4 files changed, 58 insertions(+), 8 deletions(-) + +diff --git a/dnf/base.py b/dnf/base.py +index dac3cefd..c4a4f854 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -157,8 +157,14 @@ class Base(object): + conf = dnf.conf.Conf() + subst = conf.substitutions + if 'releasever' not in subst: +- subst['releasever'] = \ +- dnf.rpm.detect_releasever(conf.installroot) ++ releasever, major, minor = \ ++ dnf.rpm.detect_releasevers(conf.installroot) ++ subst['releasever'] = releasever ++ if major is not None: ++ subst['releasever_major'] = major ++ if minor is not None: ++ subst['releasever_minor'] = minor ++ + return conf + + def _setup_modular_excludes(self): +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index e2700c09..118ce4e7 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -971,13 +971,20 @@ class Cli(object): + from_root = "/" + subst = conf.substitutions + subst.update_from_etc(from_root, varsdir=conf._get_value('varsdir')) ++ + # cachedir, logs, releasever, and gpgkey are taken from or stored in installroot ++ major = None ++ minor = None + if releasever is None and conf.releasever is None: +- releasever = dnf.rpm.detect_releasever(conf.installroot) ++ releasever, major, minor = dnf.rpm.detect_releasevers(conf.installroot) + elif releasever == '/': +- releasever = dnf.rpm.detect_releasever(releasever) ++ releasever, major, minor = dnf.rpm.detect_releasevers(releasever) + if releasever is not None: + conf.releasever = releasever ++ if major is not None: ++ conf.releasever_major = major ++ if minor is not None: ++ conf.releasever_minor = minor + if conf.releasever is None: + logger.warning(_("Unable to detect release version (use '--releasever' to specify " + "release version)")) +diff --git a/dnf/const.py.in b/dnf/const.py.in +index bcadc804..07aab7a4 100644 +--- a/dnf/const.py.in ++++ b/dnf/const.py.in +@@ -25,6 +25,8 @@ CONF_AUTOMATIC_FILENAME='/etc/dnf/automatic.conf' + DISTROVERPKG=('system-release(releasever)', 'system-release', + 'distribution-release(releasever)', 'distribution-release', + 'redhat-release', 'suse-release') ++DISTROVER_MAJOR_PKG='system-release(releasever_major)' ++DISTROVER_MINOR_PKG='system-release(releasever_minor)' + GROUP_PACKAGE_TYPES = ('mandatory', 'default', 'conditional') # :api + INSTALLONLYPKGS=['kernel', 'kernel-PAE', + 'installonlypkg(kernel)', +diff --git a/dnf/rpm/__init__.py b/dnf/rpm/__init__.py +index 12efca7f..d4be4d03 100644 +--- a/dnf/rpm/__init__.py ++++ b/dnf/rpm/__init__.py +@@ -26,12 +26,21 @@ import dnf.exceptions + import rpm # used by ansible (dnf.rpm.rpm.labelCompare in lib/ansible/modules/packaging/os/dnf.py) + + +-def detect_releasever(installroot): ++def detect_releasevers(installroot): + # :api +- """Calculate the release version for the system.""" ++ """Calculate the release version for the system, including releasever_major ++ and releasever_minor if they are overriden by the system-release-major or ++ system-release-minor provides.""" + + ts = transaction.initReadOnlyTransaction(root=installroot) + ts.pushVSFlags(~(rpm._RPMVSF_NOSIGNATURES | rpm._RPMVSF_NODIGESTS)) ++ ++ distrover_major_pkg = dnf.const.DISTROVER_MAJOR_PKG ++ distrover_minor_pkg = dnf.const.DISTROVER_MINOR_PKG ++ if dnf.pycomp.PY3: ++ distrover_major_pkg = bytes(distrover_major_pkg, 'utf-8') ++ distrover_minor_pkg = bytes(distrover_minor_pkg, 'utf-8') ++ + for distroverpkg in dnf.const.DISTROVERPKG: + if dnf.pycomp.PY3: + distroverpkg = bytes(distroverpkg, 'utf-8') +@@ -47,6 +56,8 @@ def detect_releasever(installroot): + msg = 'Error: rpmdb failed to list provides. Try: rpm --rebuilddb' + raise dnf.exceptions.Error(msg) + releasever = hdr['version'] ++ releasever_major = None ++ releasever_minor = None + try: + try: + # header returns bytes -> look for bytes +@@ -61,13 +72,37 @@ def detect_releasever(installroot): + if hdr['name'] not in (distroverpkg, distroverpkg.decode("utf8")): + # override the package version + releasever = ver ++ ++ for provide, flag, ver in zip( ++ hdr[rpm.RPMTAG_PROVIDENAME], ++ hdr[rpm.RPMTAG_PROVIDEFLAGS], ++ hdr[rpm.RPMTAG_PROVIDEVERSION]): ++ if isinstance(provide, str): ++ provide = bytes(provide, "utf-8") ++ if provide == distrover_major_pkg and flag == rpm.RPMSENSE_EQUAL and ver: ++ releasever_major = ver ++ if provide == distrover_minor_pkg and flag == rpm.RPMSENSE_EQUAL and ver: ++ releasever_minor = ver ++ + except (ValueError, KeyError, IndexError): + pass + + if is_py3bytes(releasever): + releasever = str(releasever, "utf-8") +- return releasever +- return None ++ if is_py3bytes(releasever_major): ++ releasever_major = str(releasever_major, "utf-8") ++ if is_py3bytes(releasever_minor): ++ releasever_minor = str(releasever_minor, "utf-8") ++ return releasever, releasever_major, releasever_minor ++ return (None, None, None) ++ ++ ++def detect_releasever(installroot): ++ # :api ++ """Calculate the release version for the system.""" ++ ++ releasever, _, _ = detect_releasevers(installroot) ++ return releasever + + + def _header(path): +-- +2.48.1 + diff --git a/0020-Add-releasever-major-and-releasever-minor-options.patch b/0020-Add-releasever-major-and-releasever-minor-options.patch new file mode 100644 index 0000000..6c87cec --- /dev/null +++ b/0020-Add-releasever-major-and-releasever-minor-options.patch @@ -0,0 +1,124 @@ +From 310def9d6995728d6bcaa7bc8bb5f311bcd64ca3 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 24 Jan 2025 22:50:22 +0000 +Subject: [PATCH] Add --releasever-major and --releasever-minor options + +Upstream commit: 017bbab0a253d84978f645cd358cdeb63e9ecb18 + +Allows the user to override the $releasever_major and $releasever_minor +variables on the command line, like --releasever. +--- + dnf/cli/cli.py | 29 +++++++++++++++++------------ + dnf/cli/option_parser.py | 6 ++++++ + doc/command_ref.rst | 8 ++++++++ + doc/conf_ref.rst | 2 ++ + 4 files changed, 33 insertions(+), 12 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 118ce4e7..0be9559d 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -836,7 +836,7 @@ class Cli(object): + dnf.conf.PRIO_DEFAULT) + self.demands.cacheonly = True + self.base.conf._configure_from_options(opts) +- self._read_conf_file(opts.releasever) ++ self._read_conf_file(opts.releasever, opts.releasever_major, opts.releasever_minor) + if 'arch' in opts: + self.base.conf.arch = opts.arch + self.base.conf._adjust_conf_options() +@@ -945,7 +945,7 @@ class Cli(object): + ) + ) + +- def _read_conf_file(self, releasever=None): ++ def _read_conf_file(self, releasever=None, releasever_major=None, releasever_minor=None): + timer = dnf.logging.Timer('config') + conf = self.base.conf + +@@ -973,18 +973,23 @@ class Cli(object): + subst.update_from_etc(from_root, varsdir=conf._get_value('varsdir')) + + # cachedir, logs, releasever, and gpgkey are taken from or stored in installroot +- major = None +- minor = None ++ ++ det_major = None ++ det_minor = None + if releasever is None and conf.releasever is None: +- releasever, major, minor = dnf.rpm.detect_releasevers(conf.installroot) ++ releasever, det_major, det_minor = dnf.rpm.detect_releasevers(conf.installroot) + elif releasever == '/': +- releasever, major, minor = dnf.rpm.detect_releasevers(releasever) +- if releasever is not None: +- conf.releasever = releasever +- if major is not None: +- conf.releasever_major = major +- if minor is not None: +- conf.releasever_minor = minor ++ releasever, det_major, det_minor = dnf.rpm.detect_releasevers(releasever) ++ ++ def or_else(*args): ++ for arg in args: ++ if arg is not None: ++ return arg ++ return None ++ conf.releasever = or_else(releasever, conf.releasever) ++ conf.releasever_major = or_else(releasever_major, det_major, conf.releasever_major) ++ conf.releasever_minor = or_else(releasever_minor, det_minor, conf.releasever_minor) ++ + if conf.releasever is None: + logger.warning(_("Unable to detect release version (use '--releasever' to specify " + "release version)")) +diff --git a/dnf/cli/option_parser.py b/dnf/cli/option_parser.py +index ec4696fd..fba37614 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -199,6 +199,12 @@ class OptionParser(argparse.ArgumentParser): + general_grp.add_argument("--releasever", default=None, + help=_("override the value of $releasever" + " in config and repo files")) ++ general_grp.add_argument("--releasever-major", default=None, ++ help=_("override the value of $releasever_major" ++ " in config and repo files")) ++ general_grp.add_argument("--releasever-minor", default=None, ++ help=_("override the value of $releasever_minor" ++ " in config and repo files")) + general_grp.add_argument("--setopt", dest="setopts", default=[], + action=self._SetoptsCallback, + help=_("set arbitrary config and repo options")) +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 2337a2e2..9ffd4c1c 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -334,6 +334,14 @@ Options + Configure DNF as if the distribution release was ````. This can + affect cache paths, values in configuration files and mirrorlist URLs. + ++``--releasever_major=`` ++ Override the releasever_major variable, which is usually automatically ++ detected or taken from the part of ``$releasever`` before the first ``.``. ++ ++``--releasever_minor=`` ++ Override the releasever_minor variable, which is usually automatically ++ detected or taken from the part of ``$releasever`` after the first ``.``. ++ + .. _repofrompath_options-label: + + +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index d4e4a277..441aa77b 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -503,6 +503,8 @@ configuration file by your distribution to override the DNF defaults. + + The ``$releasever_major`` and ``$releasever_minor`` variables will be automatically derived from ``$releasever`` by splitting it on the first ``.``. For example, if ``$releasever`` is set to ``1.23``, then ``$releasever_major`` will be ``1`` and ``$releasever_minor`` will be ``23``. + ++ ``$releasever_major`` and ``$releasever_minor`` can also be set by the distribution. ++ + See also :ref:`repo variables `. + + .. _reposdir-label: +-- +2.48.1 + diff --git a/0021-doc-Document-detect_releasevers-and-update-example.patch b/0021-doc-Document-detect_releasevers-and-update-example.patch new file mode 100644 index 0000000..c25862d --- /dev/null +++ b/0021-doc-Document-detect_releasevers-and-update-example.patch @@ -0,0 +1,57 @@ +From ce877f038577a0080ca9e8f69a7fe28047e829ad Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 27 Jan 2025 18:49:11 +0000 +Subject: [PATCH] doc: Document detect_releasevers and update example + +Upstream commit: 593ab0c377cdca78895628c9f7a4676ca220215c + +Adds dnf.rpm.detect_releasevers to the API docs and mention it is +now preferred over dnf.rpm.detect_releasever. + +Updates examples/install_extension.py to use detect_releasevers and set +the releasever_major and releasever_minor substitution variables. +--- + doc/api_rpm.rst | 8 ++++++++ + doc/examples/install_extension.py | 6 +++++- + 2 files changed, 13 insertions(+), 1 deletion(-) + +diff --git a/doc/api_rpm.rst b/doc/api_rpm.rst +index c59ed67d..562be41a 100644 +--- a/doc/api_rpm.rst ++++ b/doc/api_rpm.rst +@@ -27,6 +27,14 @@ + + Returns ``None`` if the information can not be determined (perhaps because the tree has no RPMDB). + ++.. function:: detect_releasevers(installroot) ++ ++ Returns a tuple of the release name, overridden major release, and overridden minor release of the distribution of the tree rooted at `installroot`. The function uses information from RPMDB found under the tree. The major and minor release versions are usually derived from the release version by splitting it on the first ``.``, but distributions can override the derived major and minor versions. It's preferred to use ``detect_releasevers`` over ``detect_releasever``; if you use the latter, you will not be aware of distribution overrides for the major and minor release versions. ++ ++ Returns ``(None, None, None)`` if the information can not be determined (perhaps because the tree has no RPMDB). ++ ++ If the distribution does not override the release major version, then the second item of the returned tuple will be ``None``. Likewise, if the release minor version is not overridden, the third return value will be ``None``. ++ + .. function:: basearch(arch) + + Return base architecture of the processor based on `arch` type given. E.g. when `arch` i686 is given then the returned value will be i386. +diff --git a/doc/examples/install_extension.py b/doc/examples/install_extension.py +index dbd3b890..b1540e12 100644 +--- a/doc/examples/install_extension.py ++++ b/doc/examples/install_extension.py +@@ -32,8 +32,12 @@ if __name__ == '__main__': + + with dnf.Base() as base: + # Substitutions are needed for correct interpretation of repo files. +- RELEASEVER = dnf.rpm.detect_releasever(base.conf.installroot) ++ RELEASEVER, MAJOR, MINOR = dnf.rpm.detect_releasever(base.conf.installroot) + base.conf.substitutions['releasever'] = RELEASEVER ++ if MAJOR is not None: ++ base.conf.substitutions['releasever_major'] = MAJOR ++ if MINOR is not None: ++ base.conf.substitutions['releasever_minor'] = MINOR + # Repositories are needed if we want to install anything. + base.read_all_repos() + # A sack is required by marking methods and dependency resolving. +-- +2.48.1 + diff --git a/0022-tests-Patch-detect_releasevers-not-detect_releasever.patch b/0022-tests-Patch-detect_releasevers-not-detect_releasever.patch new file mode 100644 index 0000000..c2a517e --- /dev/null +++ b/0022-tests-Patch-detect_releasevers-not-detect_releasever.patch @@ -0,0 +1,129 @@ +From 71fda4446391fc17e121d4c8c5eea70c981a2f0f Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 27 Jan 2025 19:20:14 +0000 +Subject: [PATCH] tests: Patch detect_releasevers, not detect_releasever + +Upstream commit: ba5ddbddf1ffb983fe9dc1a329e22eacd2ef35f7 +--- + tests/api/test_dnf_rpm.py | 4 ++++ + tests/cli/commands/test_clean.py | 2 +- + tests/support.py | 2 +- + tests/test_base.py | 4 ++-- + tests/test_cli.py | 10 +++++----- + 5 files changed, 13 insertions(+), 9 deletions(-) + +diff --git a/tests/api/test_dnf_rpm.py b/tests/api/test_dnf_rpm.py +index e6d8de84..fb606ffc 100644 +--- a/tests/api/test_dnf_rpm.py ++++ b/tests/api/test_dnf_rpm.py +@@ -14,6 +14,10 @@ class DnfRpmApiTest(TestCase): + # dnf.rpm.detect_releasever + self.assertHasAttr(dnf.rpm, "detect_releasever") + ++ def test_detect_releasevers(self): ++ # dnf.rpm.detect_releasevers ++ self.assertHasAttr(dnf.rpm, "detect_releasevers") ++ + def test_basearch(self): + # dnf.rpm.basearch + self.assertHasAttr(dnf.rpm, "basearch") +diff --git a/tests/cli/commands/test_clean.py b/tests/cli/commands/test_clean.py +index cc0a5df3..c77cb3ef 100644 +--- a/tests/cli/commands/test_clean.py ++++ b/tests/cli/commands/test_clean.py +@@ -31,7 +31,7 @@ from tests.support import mock + ''' + def _run(cli, args): + with mock.patch('sys.stdout', new_callable=StringIO), \ +- mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + cli.configure(['clean', '--config', '/dev/null'] + args) + cli.run() + +diff --git a/tests/support.py b/tests/support.py +index e50684ef..d03683ed 100644 +--- a/tests/support.py ++++ b/tests/support.py +@@ -177,7 +177,7 @@ def command_run(cmd, args): + + class Base(dnf.Base): + def __init__(self, *args, **kwargs): +- with mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + super(Base, self).__init__(*args, **kwargs) + + # mock objects +diff --git a/tests/test_base.py b/tests/test_base.py +index ad3ef675..9e0a656d 100644 +--- a/tests/test_base.py ++++ b/tests/test_base.py +@@ -57,7 +57,7 @@ class BaseTest(tests.support.TestCase): + self.assertIsNotNone(base) + base.close() + +- @mock.patch('dnf.rpm.detect_releasever', lambda x: 'x') ++ @mock.patch('dnf.rpm.detect_releasevers', lambda x: ('x', None, None)) + @mock.patch('dnf.util.am_i_root', lambda: True) + def test_default_config_root(self): + base = dnf.Base() +@@ -67,7 +67,7 @@ class BaseTest(tests.support.TestCase): + self.assertIsNotNone(reg.match(base.conf.cachedir)) + base.close() + +- @mock.patch('dnf.rpm.detect_releasever', lambda x: 'x') ++ @mock.patch('dnf.rpm.detect_releasevers', lambda x: ('x', None, None)) + @mock.patch('dnf.util.am_i_root', lambda: False) + def test_default_config_user(self): + base = dnf.Base() +diff --git a/tests/test_cli.py b/tests/test_cli.py +index 9c130c36..573d4ae2 100644 +--- a/tests/test_cli.py ++++ b/tests/test_cli.py +@@ -191,7 +191,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase): + # call setUp() once again *after* am_i_root() is mocked so the cachedir is set as expected + self.setUp() + self.base._conf.installroot = self._installroot +- with mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + self.cli.configure(['update', '-c', self.conffile]) + reg = re.compile('^' + self._installroot + '/var/tmp/dnf-[.a-zA-Z0-9_-]+$') + self.assertIsNotNone(reg.match(self.base.conf.cachedir)) +@@ -203,7 +203,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase): + def test_configure_root(self): + """ Test Cli.configure as root.""" + self.base._conf = dnf.conf.Conf() +- with mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + self.cli.configure(['update', '--nogpgcheck', '-c', self.conffile]) + reg = re.compile('^/var/cache/dnf$') + self.assertIsNotNone(reg.match(self.base.conf.system_cachedir)) +@@ -213,7 +213,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase): + + def test_configure_verbose(self): + self.base._conf.installroot = self._installroot +- with mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + self.cli.configure(['-v', 'update', '-c', self.conffile]) + parser = argparse.ArgumentParser() + expected = "%s -v update -c %s " % (parser.prog, self.conffile) +@@ -225,7 +225,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase): + @mock.patch('os.path.exists', return_value=True) + def test_conf_exists_in_installroot(self, ospathexists): + with mock.patch('logging.Logger.warning'), \ +- mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + self.cli.configure(['--installroot', '/roots/dnf', 'update']) + self.assertEqual(self.base.conf.config_file_path, '/roots/dnf/etc/dnf/dnf.conf') + self.assertEqual(self.base.conf.installroot, '/roots/dnf') +@@ -233,7 +233,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase): + @mock.patch('dnf.cli.cli.Cli._parse_commands', new=mock.MagicMock) + @mock.patch('os.path.exists', return_value=False) + def test_conf_notexists_in_installroot(self, ospathexists): +- with mock.patch('dnf.rpm.detect_releasever', return_value=69): ++ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)): + self.cli.configure(['--installroot', '/roots/dnf', 'update']) + self.assertEqual(self.base.conf.config_file_path, '/etc/dnf/dnf.conf') + self.assertEqual(self.base.conf.installroot, '/roots/dnf') +-- +2.48.1 + diff --git a/0023-Document-how-releasever-releasever_-major-minor-affe.patch b/0023-Document-how-releasever-releasever_-major-minor-affe.patch new file mode 100644 index 0000000..025c3ef --- /dev/null +++ b/0023-Document-how-releasever-releasever_-major-minor-affe.patch @@ -0,0 +1,97 @@ +From 0d750818c8aa92fd08dd5179839f5734a1b1be96 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 4 Feb 2025 23:01:43 +0000 +Subject: [PATCH] Document how --releasever, --releasever_{major,minor} affect + each other + +Upstream commit: e931960d26a0782a23e8d89a6a662ee2442153fc +--- + dnf/conf/config.py | 16 ++++++++++++++++ + doc/command_ref.rst | 2 ++ + tests/test_config.py | 3 +++ + 3 files changed, 21 insertions(+) + +diff --git a/dnf/conf/config.py b/dnf/conf/config.py +index 6cf28724..7e5e8a38 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -425,6 +425,12 @@ class MainConf(BaseConfig): + @releasever.setter + def releasever(self, val): + # :api ++ """ ++ Sets the releasever variable and sets releasever_major and ++ releasever_minor accordingly. releasever_major is set to the part of ++ $releasever before the first ".". releasever_minor is set to the part ++ after the first ".". ++ """ + if val is None: + self.substitutions.pop('releasever', None) + return +@@ -438,6 +444,11 @@ class MainConf(BaseConfig): + @releasever_major.setter + def releasever_major(self, val): + # :api ++ """ ++ Override the releasever_major variable, which is usually derived from ++ the releasever variable. This setter does not update the value of ++ $releasever. ++ """ + if val is None: + self.substitutions.pop('releasever_major', None) + return +@@ -446,6 +457,11 @@ class MainConf(BaseConfig): + @property + def releasever_minor(self): + # :api ++ """ ++ Override the releasever_minor variable, which is usually derived from ++ the releasever variable. This setter does not update the value of ++ $releasever. ++ """ + return self.substitutions.get('releasever_minor') + + @releasever_minor.setter +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 9ffd4c1c..f7b8e22c 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -337,10 +337,12 @@ Options + ``--releasever_major=`` + Override the releasever_major variable, which is usually automatically + detected or taken from the part of ``$releasever`` before the first ``.``. ++ This option does not affect the ``$releasever`` variable. + + ``--releasever_minor=`` + Override the releasever_minor variable, which is usually automatically + detected or taken from the part of ``$releasever`` after the first ``.``. ++ This option does not affect the ``$releasever`` variable. + + .. _repofrompath_options-label: + +diff --git a/tests/test_config.py b/tests/test_config.py +index 16bdcccb..69ba988c 100644 +--- a/tests/test_config.py ++++ b/tests/test_config.py +@@ -149,15 +149,18 @@ class ConfTest(tests.support.TestCase): + def test_releasever_major_minor(self): + conf = Conf() + conf.releasever = '1.2' ++ self.assertEqual(conf.releasever, '1.2') + self.assertEqual(conf.releasever_major, '1') + self.assertEqual(conf.releasever_minor, '2') + + # override releasever_major + conf.releasever_major = '3' ++ self.assertEqual(conf.releasever, '1.2') + self.assertEqual(conf.releasever_major, '3') + self.assertEqual(conf.releasever_minor, '2') + + # override releasever_minor + conf.releasever_minor = '4' ++ self.assertEqual(conf.releasever, '1.2') + self.assertEqual(conf.releasever_major, '3') + self.assertEqual(conf.releasever_minor, '4') +-- +2.48.1 + diff --git a/dnf.spec b/dnf.spec index 1bfdc53..98ca279 100644 --- a/dnf.spec +++ b/dnf.spec @@ -2,7 +2,7 @@ %define __cmake_in_source_build 1 # default dependencies -%global hawkey_version 0.73.1 +%global hawkey_version 0.74.0 %global libcomps_version 0.1.8 %global libmodulemd_version 2.9.3 %global rpm_version 4.14.0 @@ -23,6 +23,10 @@ %global rpm_version 4.11.3-25.el7.centos.1 %endif +%if 0%{?rhel} == 10 + %global hawkey_version 0.73.1-8 +%endif + # override dependencies for fedora 26 %if 0%{?fedora} == 26 %global rpm_version 4.13.0.1-7 @@ -68,7 +72,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.20.0 -Release: 6%{?dist} +Release: 12%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPL-2.0-or-later AND GPL-1.0-only @@ -79,6 +83,25 @@ Patch2: 0002-Limit-queries-to-nevra-forms-when-provided-by-comman.patch Patch3: 0003-doc-Remove-provide-of-spec-definition-for-repoquery-.patch Patch4: 0004-Drop-collect-file-for-ABRT.patch Patch5: 0005-tests-Use-PGP-keys-without-SHA-1.patch +Patch6: 0006-Add-detection-for-ostree-based-systems-and-warn-user.patch +Patch7: 0007-Update-ostree-bootc-host-system-check.patch +Patch8: 0008-Update-bootc-hosts-message-to-point-to-bootc-help.patch +Patch9: 0009-Allow-installroot-on-read-only-bootc-system.patch +Patch10: 0010-Allow-downloadonly-on-read-only-bootc-system.patch +Patch11: 0011-Automatic-check-availability-of-config-file.patch +Patch12: 0012-Add-support-for-transient.patch +Patch13: 0013-bootc-Document-transient-and-persistence.patch +Patch14: 0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch +Patch15: 0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch +Patch16: 0016-spec-Add-dnf-bootc-subpackage.patch +Patch17: 0017-Require-libdnf-0.74.0-with-persistence-option.patch +Patch18: 0018-Derive-releasever_-major-minor-in-conf-not-substitut.patch +Patch19: 0019-Override-releasever_-major-minor-with-provides.patch +Patch20: 0020-Add-releasever-major-and-releasever-minor-options.patch +Patch21: 0021-doc-Document-detect_releasevers-and-update-example.patch +Patch22: 0022-tests-Patch-detect_releasevers-not-detect_releasever.patch +Patch23: 0023-Document-how-releasever-releasever_-major-minor-affe.patch + BuildArch: noarch BuildRequires: cmake BuildRequires: gettext @@ -196,6 +219,17 @@ Requires: python3-%{name} = %{version}-%{release} %description automatic Systemd units that can periodically download package upgrades and apply them. +%package bootc +Summary: %{pkg_summary} - additional bootc dependencies +Requires: python3-%{name} = %{version}-%{release} +Requires: ostree +Requires: ostree-libs +Requires: python3-gobject-base +Requires: util-linux-core + +%description bootc +Additional dependencies needed to perform transactions on booted bootc (bootable containers) systems. + %prep %autosetup -p1 @@ -418,7 +452,31 @@ popd %{_unitdir}/%{name}-automatic-install.timer %{python3_sitelib}/%{name}/automatic/ +%files bootc +# bootc subpackage does not include any files + %changelog +* Fri Feb 07 2025 Carl George - 4.20.0-12 +- Override releasever_{major,minor} with system-release provides (RHEL-68034) + +* Thu Feb 06 2025 Petr Pisar - 4.20.0-11 +- Add support for transient transactions (RHEL-76849) + +* Fri Dec 13 2024 Marek Blaha - 4.20.0-10 +- automatic: Check availability of config file + +* Tue Oct 29 2024 Troy Dawson - 4.20.0-9 +- Bump release for October 2024 mass rebuild: + Resolves: RHEL-64018 + +* Tue Oct 15 2024 Petr Pisar - 4.20.0-8 +- Allow "dnf install --downloadonly" on locked OSTree and bootc systems + (RHEL-62028) + +* Fri Sep 20 2024 Petr Pisar - 4.20.0-7 +- More specific error message on a locked OSTree system or a bootc system + without a usr-overlay (RHEL-49671) + * Tue Aug 06 2024 Petr Pisar - 4.20.0-6 - Revert more specific error message on a locked OSTree system or a bootc system without a usr-overlay (RHEL-49671)