diff --git a/SOURCES/0045-package-remote_location-takes-basedir-into-account.patch b/SOURCES/0045-package-remote_location-takes-basedir-into-account.patch new file mode 100644 index 0000000..b0facc7 --- /dev/null +++ b/SOURCES/0045-package-remote_location-takes-basedir-into-account.patch @@ -0,0 +1,35 @@ +From 5d410dee9815d3ab23e64a48871c8b9aac717127 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Wed, 18 Sep 2024 09:46:20 +0200 +Subject: [PATCH] package: remote_location() takes basedir into account + +If the package location in the repodata contains basedir, it needs to be +taken into account when calculating the package's remote_location. + +Resolves: https://github.com/rpm-software-management/dnf/issues/2130 +Resolves: RHEL-71125 + += changelog = +msg: Fix package location if baseurl is present in the metadata +type: bugfix +resolves: https://github.com/rpm-software-management/dnf/issues/2130 +--- + dnf/package.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/dnf/package.py b/dnf/package.py +index fc89cf98..5912f79c 100644 +--- a/dnf/package.py ++++ b/dnf/package.py +@@ -300,6 +300,8 @@ class Package(hawkey.Package): + """ + if self._from_system or self._from_cmdline: + return None ++ if self.baseurl: ++ return os.path.join(self.baseurl, self.location.lstrip("/")) + return self.repo.remote_location(self.location, schemes) + + def _is_local_pkg(self): +-- +2.48.1 + diff --git a/SOURCES/0046-Usage-help-don-t-mark-mandatory-option-parameters-as.patch b/SOURCES/0046-Usage-help-don-t-mark-mandatory-option-parameters-as.patch new file mode 100644 index 0000000..fcff2ea --- /dev/null +++ b/SOURCES/0046-Usage-help-don-t-mark-mandatory-option-parameters-as.patch @@ -0,0 +1,129 @@ +From f777d26ba70778ff015a3f3af21f76e5a1e8473b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Tue, 11 Feb 2025 14:33:08 +0100 +Subject: [PATCH] Usage help: don't mark mandatory option parameters as + optional + +For: https://issues.redhat.com/browse/RHEL-63958 +--- + dnf/cli/commands/repoquery.py | 2 +- + dnf/cli/option_parser.py | 26 +++++++++++++------------- + 2 files changed, 14 insertions(+), 14 deletions(-) + +diff --git a/dnf/cli/commands/repoquery.py b/dnf/cli/commands/repoquery.py +index 41dd688e..a5e55122 100644 +--- a/dnf/cli/commands/repoquery.py ++++ b/dnf/cli/commands/repoquery.py +@@ -126,7 +126,7 @@ class RepoQueryCommand(commands.Command): + parser.add_argument('--show-duplicates', action='store_true', + help=_("Query all versions of packages (default)")) + parser.add_argument('--arch', '--archlist', dest='arches', default=[], +- action=_CommaSplitCallback, metavar='[arch]', ++ action=_CommaSplitCallback, metavar='ARCH', + help=_('show only results from this ARCH')) + parser.add_argument('-f', '--file', metavar='FILE', nargs='+', + help=_('show only results that owns FILE')) +diff --git a/dnf/cli/option_parser.py b/dnf/cli/option_parser.py +index fba37614..c95d3d99 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -171,7 +171,7 @@ class OptionParser(argparse.ArgumentParser): + general_grp = self.add_argument_group(_('General {prog} options'.format( + prog=dnf.util.MAIN_PROG_UPPER))) + general_grp.add_argument("-c", "--config", dest="config_file_path", +- default=None, metavar='[config file]', ++ default=None, metavar='CONFIG_FILE', + help=_("config file location")) + general_grp.add_argument("-q", "--quiet", dest="quiet", + action="store_true", default=None, +@@ -182,7 +182,7 @@ class OptionParser(argparse.ArgumentParser): + help=_("show {prog} version and exit").format( + prog=dnf.util.MAIN_PROG_UPPER)) + general_grp.add_argument("--installroot", help=_("set install root"), +- metavar='[path]') ++ metavar='PATH]') + general_grp.add_argument("--nodocs", action="store_const", const=['nodocs'], dest='tsflags', + help=_("do not install documentations")) + general_grp.add_argument("--noplugins", action="store_false", +@@ -191,11 +191,11 @@ class OptionParser(argparse.ArgumentParser): + general_grp.add_argument("--enableplugin", dest="enableplugin", + default=[], action=self._SplitCallback, + help=_("enable plugins by name"), +- metavar='[plugin]') ++ metavar='PLUGIN]') + general_grp.add_argument("--disableplugin", dest="disableplugin", + default=[], action=self._SplitCallback, + help=_("disable plugins by name"), +- metavar='[plugin]') ++ metavar='PLUGIN]') + general_grp.add_argument("--releasever", default=None, + help=_("override the value of $releasever" + " in config and repo files")) +@@ -229,10 +229,10 @@ class OptionParser(argparse.ArgumentParser): + help=_("run entirely from system cache, " + "don't update cache")) + general_grp.add_argument("-R", "--randomwait", dest="sleeptime", type=int, +- default=None, metavar='[minutes]', ++ default=None, metavar='MINUTES', + help=_("maximum command wait time")) + general_grp.add_argument("-d", "--debuglevel", dest="debuglevel", +- metavar='[debug level]', default=None, ++ metavar='DEBUG_LEVEL', default=None, + help=_("debugging output level"), type=int) + general_grp.add_argument("--debugsolver", + action="store_true", default=None, +@@ -252,7 +252,7 @@ class OptionParser(argparse.ArgumentParser): + "repoquery").format(prog=dnf.util.MAIN_PROG)) + general_grp.add_argument("--rpmverbosity", default=None, + help=_("debugging output level for rpm"), +- metavar='[debug level name]') ++ metavar='DEBUG_LEVEL_NAME') + general_grp.add_argument("-y", "--assumeyes", action="store_true", + default=None, help=_("automatically answer yes" + " for all questions")) +@@ -260,20 +260,20 @@ class OptionParser(argparse.ArgumentParser): + default=None, help=_("automatically answer no" + " for all questions")) + general_grp.add_argument("--enablerepo", action=self._RepoCallback, +- dest='repos_ed', default=[], metavar='[repo]', ++ dest='repos_ed', default=[], metavar='REPO', + help=_("Temporarily enable repositories for the purpose " + "of the current dnf command. Accepts an id, a " + "comma-separated list of ids, or a glob of ids. " + "This option can be specified multiple times.")) + repo_group = general_grp.add_mutually_exclusive_group() + repo_group.add_argument("--disablerepo", action=self._RepoCallback, +- dest='repos_ed', default=[], metavar='[repo]', ++ dest='repos_ed', default=[], metavar='REPO', + help=_("Temporarily disable active repositories for the " + "purpose of the current dnf command. Accepts an id, " + "a comma-separated list of ids, or a glob of ids. " + "This option can be specified multiple times, but " + "is mutually exclusive with `--repo`.")) +- repo_group.add_argument('--repo', '--repoid', metavar='[repo]', dest='repo', ++ repo_group.add_argument('--repo', '--repoid', metavar='REPO', dest='repo', + action=self._SplitCallback, default=[], + help=_('enable just specific repositories by an id or a glob, ' + 'can be specified multiple times')) +@@ -289,15 +289,15 @@ class OptionParser(argparse.ArgumentParser): + general_grp.add_argument("-x", "--exclude", "--excludepkgs", default=[], + dest='excludepkgs', action=self._SplitCallback, + help=_("exclude packages by name or glob"), +- metavar='[package]') ++ metavar='PACKAGE') + general_grp.add_argument("--disableexcludes", "--disableexcludepkgs", + default=[], dest="disable_excludes", + action=self._SplitCallback, + help=_("disable excludepkgs"), +- metavar='[repo]') ++ metavar='{all, main, REPOID}') + general_grp.add_argument("--repofrompath", default={}, + action=self._SplitExtendDictCallback, +- metavar='[repo,path]', ++ metavar='REPO,PATH', + help=_("label and path to an additional repository to use (same " + "path as in a baseurl), can be specified multiple times.")) + general_grp.add_argument("--noautoremove", action="store_false", +-- +2.48.1 + diff --git a/SOURCES/0047-Fix-typo-from-previous-commit-left-over.patch b/SOURCES/0047-Fix-typo-from-previous-commit-left-over.patch new file mode 100644 index 0000000..80c3b18 --- /dev/null +++ b/SOURCES/0047-Fix-typo-from-previous-commit-left-over.patch @@ -0,0 +1,39 @@ +From d2d3770168e744eeace6c36b38f4ba0709c7b9c0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Mon, 28 Apr 2025 12:23:27 +0200 +Subject: [PATCH 1/2] Fix typo from previous commit (left over `]`) + +--- + dnf/cli/option_parser.py | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/dnf/cli/option_parser.py b/dnf/cli/option_parser.py +index a23c8553..6bb32c51 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -185,7 +185,7 @@ class OptionParser(argparse.ArgumentParser): + help=_("show {prog} version and exit").format( + prog=dnf.util.MAIN_PROG_UPPER)) + general_grp.add_argument("--installroot", help=_("set install root"), +- metavar='PATH]') ++ metavar='PATH') + general_grp.add_argument("--nodocs", action="store_const", const=['nodocs'], dest='tsflags', + help=_("do not install documentations")) + general_grp.add_argument("--noplugins", action="store_false", +@@ -194,11 +194,11 @@ class OptionParser(argparse.ArgumentParser): + general_grp.add_argument("--enableplugin", dest="enableplugin", + default=[], action=self._SplitCallback, + help=_("enable plugins by name"), +- metavar='PLUGIN]') ++ metavar='PLUGIN') + general_grp.add_argument("--disableplugin", dest="disableplugin", + default=[], action=self._SplitCallback, + help=_("disable plugins by name"), +- metavar='PLUGIN]') ++ metavar='PLUGIN') + general_grp.add_argument("--releasever", default=None, + help=_("override the value of $releasever" + " in config and repo files")) +-- +2.48.1 + diff --git a/SOURCES/0048-disableexcludes-and-disableexcludepkgs-values-are-no.patch b/SOURCES/0048-disableexcludes-and-disableexcludepkgs-values-are-no.patch new file mode 100644 index 0000000..daa4e26 --- /dev/null +++ b/SOURCES/0048-disableexcludes-and-disableexcludepkgs-values-are-no.patch @@ -0,0 +1,26 @@ +From 0f37439d04cfa6f2b39d3602b1b40570d37af80f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Mon, 28 Apr 2025 12:41:13 +0200 +Subject: [PATCH 2/2] `--disableexcludes` and `--disableexcludepkgs` values are + not optional + +--- + doc/command_ref.rst | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 8b55d5a7..30627bd4 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -164,7 +164,7 @@ Options + + .. _disableexcludes-label: + +-``--disableexcludes=[all|main|], --disableexcludepkgs=[all|main|]`` ++``--disableexcludes={all|main|}, --disableexcludepkgs={all|main|}`` + Disable ``excludepkgs`` and ``includepkgs`` configuration options. Takes one of the following three options: + + * ``all``, disables all ``excludepkgs`` and ``includepkgs`` configurations +-- +2.48.1 + diff --git a/SOURCES/0049-bootc-tmt-testing.patch b/SOURCES/0049-bootc-tmt-testing.patch new file mode 100644 index 0000000..9b875fc --- /dev/null +++ b/SOURCES/0049-bootc-tmt-testing.patch @@ -0,0 +1,37 @@ +From 72264e6ad00f90e7e261657f79dee7bae3ceb7e0 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 10 Apr 2025 20:18:44 +0000 +Subject: [PATCH 1/8] bootc tmt testing + +--- + .packit.yaml | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + create mode 100644 .packit.yaml + +diff --git a/.packit.yaml b/.packit.yaml +new file mode 100644 +index 000000000..0738de205 +--- /dev/null ++++ b/.packit.yaml +@@ -0,0 +1,18 @@ ++# See the documentation for more information: ++# https://packit.dev/docs/configuration/ ++ ++specfile_path: dnf.spec ++ ++jobs: ++ - job: copr_build ++ trigger: pull_request ++ targets: ++ - centos-stream-9-x86_64 ++ - job: tests ++ trigger: pull_request ++ identifier: "dnf-tests" ++ targets: ++ - centos-stream-9-x86_64 ++ fmf_url: https://github.com/evan-goode/ci-dnf-stack.git ++ fmf_ref: evan-goode/bootc ++ tmt_plan: "^/plans/integration/bootc-behave-dnf$" +-- +2.49.0 + diff --git a/SOURCES/0050-persistence-store-persist-transient-in-history-DB.patch b/SOURCES/0050-persistence-store-persist-transient-in-history-DB.patch new file mode 100644 index 0000000..2fe4443 --- /dev/null +++ b/SOURCES/0050-persistence-store-persist-transient-in-history-DB.patch @@ -0,0 +1,117 @@ +From 5f91b890799559d0a8fa5861ff8f5e2a16db1dbf Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 15 May 2025 20:48:58 +0000 +Subject: [PATCH 2/8] persistence: store persist/transient in history DB + +For all transactions, store whether the transaction was persistent or +transient in the history DB. Requires libdnf 0.75.0. + +Moves some of the bootc logic to the `configure` phase. + +For https://github.com/rpm-software-management/dnf/issues/2196. +--- + dnf.spec | 2 +- + dnf/base.py | 6 ++++-- + dnf/cli/cli.py | 3 +++ + dnf/db/history.py | 8 +++++++- + 4 files changed, 15 insertions(+), 4 deletions(-) + +diff --git a/dnf.spec b/dnf.spec +index 28fac9a09..4abc0084b 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -2,7 +2,7 @@ + %define __cmake_in_source_build 1 + + # default dependencies +-%global hawkey_version 0.74.0 ++%global hawkey_version 0.75.0 + %global libcomps_version 0.1.8 + %global libmodulemd_version 2.9.3 + %global rpm_version 4.14.0 +diff --git a/dnf/base.py b/dnf/base.py +index 168207b5f..d0ce6364c 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -118,6 +118,7 @@ class Base(object): + self._update_security_options = {} + self._allow_erasing = False + self._repo_set_imported_gpg_keys = set() ++ self._persistence = libdnf.transaction.TransactionPersistence_UNKNOWN + self.output = None + + def __enter__(self): +@@ -964,7 +965,7 @@ class Base(object): + else: + rpmdb_version = old.end_rpmdb_version + +- self.history.beg(rpmdb_version, [], [], cmdline) ++ self.history.beg(rpmdb_version, [], [], cmdline=cmdline, persistence=self._persistence) + self.history.end(rpmdb_version) + self._plugins.run_pre_transaction() + self._plugins.run_transaction() +@@ -1115,7 +1116,8 @@ class Base(object): + cmdline = ' '.join(self.cmds) + + comment = self.conf.comment if self.conf.comment else "" +- tid = self.history.beg(rpmdbv, using_pkgs, [], cmdline, comment) ++ tid = self.history.beg(rpmdbv, using_pkgs, [], cmdline=cmdline, ++ comment=comment, persistence=self._persistence) + + if self.conf.reset_nice: + onice = os.nice(0) +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 23170a82b..99ed1f282 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -244,11 +244,14 @@ 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.")) ++ self._persistence = libdnf.transaction.TransactionPersistence_TRANSIENT + else: + # Not a bootc transaction. + if self.conf.persistence == "transient": + raise CliError(_("Transient transactions are only supported on bootc systems.")) + ++ self._persistence = libdnf.transaction.TransactionPersistence_PERSIST ++ + if self._promptWanted(): + if self.conf.assumeno or not self.output.userconfirm(): + raise CliError(_("Operation aborted.")) +diff --git a/dnf/db/history.py b/dnf/db/history.py +index bf9020ad0..2cde9cbc8 100644 +--- a/dnf/db/history.py ++++ b/dnf/db/history.py +@@ -222,6 +222,10 @@ class TransactionWrapper(object): + def comment(self): + return self._trans.getComment() + ++ @property ++ def persistence(self): ++ return self._trans.getPersistence() ++ + def tids(self): + return [self._trans.getId()] + +@@ -418,7 +422,8 @@ class SwdbInterface(object): + # return result + + # TODO: rename to begin_transaction? +- def beg(self, rpmdb_version, using_pkgs, tsis, cmdline=None, comment=""): ++ def beg(self, rpmdb_version, using_pkgs, tsis, cmdline=None, comment="", ++ persistence=libdnf.transaction.TransactionPersistence_UNKNOWN): + try: + self.swdb.initTransaction() + except: +@@ -431,6 +436,7 @@ class SwdbInterface(object): + int(misc.getloginuid()), + comment) + self.swdb.setReleasever(self.releasever) ++ self.swdb.setPersistence(persistence) + self._tid = tid + + return tid +-- +2.49.0 + diff --git a/SOURCES/0051-Print-persist-or-transient-in-history-info.patch b/SOURCES/0051-Print-persist-or-transient-in-history-info.patch new file mode 100644 index 0000000..af73ac7 --- /dev/null +++ b/SOURCES/0051-Print-persist-or-transient-in-history-info.patch @@ -0,0 +1,31 @@ +From 5f7fcae28541368d80fab9c9224c7974726a6b10 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 16 May 2025 20:38:56 +0000 +Subject: [PATCH 3/8] Print "persist" or "transient" in history info + +--- + dnf/cli/output.py | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 7f1d62c5a..6c97c7dc9 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -1772,6 +1772,14 @@ Transaction Summary + else: + print(_("Command Line :"), old.cmdline) + ++ if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: ++ persistence_str = "Persist" ++ elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: ++ persistence_str = "Transient" ++ else: ++ persistence_str = "Unknown" ++ print(_("Persistence :"), persistence_str) ++ + if old.comment is not None: + if isinstance(old.comment, (list, tuple)): + for comment in old.comment: +-- +2.49.0 + diff --git a/SOURCES/0052-history-persistence-for-MergedTransaction.patch b/SOURCES/0052-history-persistence-for-MergedTransaction.patch new file mode 100644 index 0000000..2d98311 --- /dev/null +++ b/SOURCES/0052-history-persistence-for-MergedTransaction.patch @@ -0,0 +1,58 @@ +From ceaf4718a6c7435050f5bfb451077683baf01600 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 19 May 2025 22:36:50 +0000 +Subject: [PATCH 4/8] history: persistence for MergedTransaction + +--- + dnf/cli/output.py | 18 ++++++++++++------ + dnf/db/history.py | 4 ++++ + 2 files changed, 16 insertions(+), 6 deletions(-) + +diff --git a/dnf/cli/output.py b/dnf/cli/output.py +index 6c97c7dc9..820ab88fa 100644 +--- a/dnf/cli/output.py ++++ b/dnf/cli/output.py +@@ -1772,13 +1772,19 @@ Transaction Summary + else: + print(_("Command Line :"), old.cmdline) + +- if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: +- persistence_str = "Persist" +- elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: +- persistence_str = "Transient" ++ def print_persistence(persistence): ++ if old.persistence == libdnf.transaction.TransactionPersistence_PERSIST: ++ persistence_str = "Persist" ++ elif old.persistence == libdnf.transaction.TransactionPersistence_TRANSIENT: ++ persistence_str = "Transient" ++ else: ++ persistence_str = "Unknown" ++ print(_("Persistence :"), persistence_str) ++ if isinstance(old.persistence, (list, tuple)): ++ for persistence in old.persistence: ++ print_persistence(persistence) + else: +- persistence_str = "Unknown" +- print(_("Persistence :"), persistence_str) ++ print_persistence(old.persistence) + + if old.comment is not None: + if isinstance(old.comment, (list, tuple)): +diff --git a/dnf/db/history.py b/dnf/db/history.py +index 2cde9cbc8..a2c1a8882 100644 +--- a/dnf/db/history.py ++++ b/dnf/db/history.py +@@ -269,6 +269,10 @@ class MergedTransactionWrapper(TransactionWrapper): + def cmdline(self): + return self._trans.listCmdlines() + ++ @property ++ def persistence(self): ++ return self._trans.listPersistences() ++ + @property + def releasever(self): + return self._trans.listReleasevers() +-- +2.49.0 + diff --git a/SOURCES/0053-bootc-Check-whether-protected-paths-will-be-modified.patch b/SOURCES/0053-bootc-Check-whether-protected-paths-will-be-modified.patch new file mode 100644 index 0000000..ca14769 --- /dev/null +++ b/SOURCES/0053-bootc-Check-whether-protected-paths-will-be-modified.patch @@ -0,0 +1,52 @@ +From abce1aabea6a28ef1d49a6530c521f9a48eedebb Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 28 May 2025 20:46:56 +0000 +Subject: [PATCH 5/8] bootc: Check whether protected paths will be modified + +For https://github.com/rpm-software-management/dnf/issues/2199. + +Requires libdnf 0.75.0 with the new `usr_drift_protected_paths` option. +--- + dnf/cli/cli.py | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 99ed1f282..74cf418c4 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -29,6 +29,7 @@ try: + from collections.abc import Sequence + except ImportError: + from collections import Sequence ++from collections import defaultdict + import datetime + import logging + import operator +@@ -245,6 +246,24 @@ class BaseCli(dnf.Base): + "Keep in mind that changes to /etc and /var will still persist, and packages " + "commonly modify these directories.")) + self._persistence = libdnf.transaction.TransactionPersistence_TRANSIENT ++ ++ # Check whether the transaction modifies usr_drift_protected_paths ++ transaction_protected_paths = defaultdict(list) ++ for pkg in trans: ++ for pkg_file_path in sorted(pkg.files): ++ for protected_path in self.conf.usr_drift_protected_paths: ++ if pkg_file_path.startswith("%s/" % protected_path) or pkg_file_path == protected_path: ++ transaction_protected_paths[pkg.nevra].append(pkg_file_path) ++ if transaction_protected_paths: ++ logger.info(_('This operation would modify the following paths, possibly introducing ' ++ 'inconsistencies when the transient overlay on /usr is discarded. See the ' ++ 'usr_drift_protected_paths configuration option for more information.')) ++ for nevra, protected_paths in transaction_protected_paths.items(): ++ logger.info(nevra) ++ for protected_path in protected_paths: ++ logger.info(" %s" % protected_path) ++ raise CliError(_("Operation aborted.")) ++ + else: + # Not a bootc transaction. + if self.conf.persistence == "transient": +-- +2.49.0 + diff --git a/SOURCES/0054-spec-package-etc-dnf-usr_drift_protected_paths.d.patch b/SOURCES/0054-spec-package-etc-dnf-usr_drift_protected_paths.d.patch new file mode 100644 index 0000000..da77bb1 --- /dev/null +++ b/SOURCES/0054-spec-package-etc-dnf-usr_drift_protected_paths.d.patch @@ -0,0 +1,57 @@ +From 1a47a316ef937f5f04e5f82e64c5aef1db45c717 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 3 Jun 2025 22:43:46 +0000 +Subject: [PATCH 6/8] spec: package /etc/dnf/usr_drift_protected_paths.d + +--- + dnf.spec | 1 + + dnf/cli/cli.py | 2 +- + etc/dnf/CMakeLists.txt | 1 + + etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt | 1 + + 4 files changed, 4 insertions(+), 1 deletion(-) + create mode 100644 etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt + +diff --git a/dnf.spec b/dnf.spec +index 4abc0084b..da5d5fd28 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -301,6 +301,7 @@ popd + %dir %{confdir}/modules.defaults.d + %dir %{pluginconfpath} + %dir %{confdir}/protected.d ++%dir %{confdir}/usr-drift-protected-paths.d + %dir %{confdir}/vars + %dir %{confdir}/aliases.d + %exclude %{confdir}/aliases.d/zypper.conf +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 74cf418c4..5602a07b1 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -262,7 +262,7 @@ class BaseCli(dnf.Base): + logger.info(nevra) + for protected_path in protected_paths: + logger.info(" %s" % protected_path) +- raise CliError(_("Operation aborted.")) ++ raise CliError(_("Operation aborted. Pass --setopt=usr_drift_protected_paths= to disable this check and proceed anyway.")) + + else: + # Not a bootc transaction. +diff --git a/etc/dnf/CMakeLists.txt b/etc/dnf/CMakeLists.txt +index 6f0eec94e..7a60186d7 100644 +--- a/etc/dnf/CMakeLists.txt ++++ b/etc/dnf/CMakeLists.txt +@@ -1,3 +1,4 @@ + INSTALL (FILES "dnf-strict.conf" "dnf.conf" "automatic.conf" DESTINATION ${SYSCONFDIR}/dnf) + ADD_SUBDIRECTORY (aliases.d) + ADD_SUBDIRECTORY (protected.d) ++ADD_SUBDIRECTORY (usr-drift-protected-paths.d) +diff --git a/etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt b/etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt +new file mode 100644 +index 000000000..206b1b281 +--- /dev/null ++++ b/etc/dnf/usr-drift-protected-paths.d/CMakeLists.txt +@@ -0,0 +1 @@ ++INSTALL(DIRECTORY DESTINATION ${SYSCONFDIR}/dnf/usr-drift-protected-paths.d) +-- +2.49.0 + diff --git a/SOURCES/0055-Support-globs-in-usr_drift_protected_paths.patch b/SOURCES/0055-Support-globs-in-usr_drift_protected_paths.patch new file mode 100644 index 0000000..03702c3 --- /dev/null +++ b/SOURCES/0055-Support-globs-in-usr_drift_protected_paths.patch @@ -0,0 +1,35 @@ +From 26a9f2163c0d3828f474537687ead4e100f5911a Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 6 Jun 2025 21:24:20 +0000 +Subject: [PATCH 7/8] Support globs in usr_drift_protected_paths + +--- + dnf/cli/cli.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 5602a07b1..c41f31ed6 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -31,6 +31,7 @@ except ImportError: + from collections import Sequence + from collections import defaultdict + import datetime ++from fnmatch import fnmatch + import logging + import operator + import os +@@ -251,8 +252,8 @@ class BaseCli(dnf.Base): + transaction_protected_paths = defaultdict(list) + for pkg in trans: + for pkg_file_path in sorted(pkg.files): +- for protected_path in self.conf.usr_drift_protected_paths: +- if pkg_file_path.startswith("%s/" % protected_path) or pkg_file_path == protected_path: ++ for protected_pattern in self.conf.usr_drift_protected_paths: ++ if fnmatch(pkg_file_path, protected_pattern): + transaction_protected_paths[pkg.nevra].append(pkg_file_path) + if transaction_protected_paths: + logger.info(_('This operation would modify the following paths, possibly introducing ' +-- +2.49.0 + diff --git a/SOURCES/0056-doc-Document-usr_drift_protected_paths.patch b/SOURCES/0056-doc-Document-usr_drift_protected_paths.patch new file mode 100644 index 0000000..05e4bf0 --- /dev/null +++ b/SOURCES/0056-doc-Document-usr_drift_protected_paths.patch @@ -0,0 +1,32 @@ +From cb765957d546f7d28aef270885418afea4906b89 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 6 Jun 2025 22:31:27 +0000 +Subject: [PATCH 8/8] doc: Document `usr_drift_protected_paths` + +--- + doc/conf_ref.rst | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index a34e355b6..7ff286fee 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -549,6 +549,15 @@ configuration file by your distribution to override the DNF defaults. + + Set this to False to disable the automatic running of ``group upgrade`` when running the ``upgrade`` command. Default is ``True`` (perform the operation). + ++.. _usr_drift_protected_paths-label: ++ ++``usr_drift_protected_paths`` ++ :ref:`list ` ++ ++ List of paths that are likely to cause problems when their contents drift with respect to ``/usr``, e.g. ``/etc/pam.d/*``. If a transient transaction would modify these paths, DNF aborts the operation and prints an error. Supports globs. Defaults to ``glob:/etc/dnf/usr-drift-protected-paths.d/*.conf``. So a list of paths can be protected by creating a ``.conf`` file in ``/etc/dnf/usr-drift-protected-paths.d/`` containing one path (or glob pattern) per line. ++ ++ When using ``persistence=transient`` on bootc systems, a transient overlay is created on ``/usr``, and any changes DNF makes to ``/usr`` will be discarded on reboot. However, other paths such as ``/etc`` and ``/var`` are (often) not backed by a transient overlay, so changes to them will persist across reboots. Usually, this "filesystem drift" is fine, but it can cause problems in certain situations. For example, a configuration file in ``/etc`` that's shared by multiple packages might reference a ``.so`` file under ``/usr/lib64`` that no longer exists. ++ + .. _varsdir_options-label: + + ``varsdir`` +-- +2.49.0 + diff --git a/SOURCES/0057-conf-Add-test-for-shell-like-variable-expansion.patch b/SOURCES/0057-conf-Add-test-for-shell-like-variable-expansion.patch new file mode 100644 index 0000000..58aa962 --- /dev/null +++ b/SOURCES/0057-conf-Add-test-for-shell-like-variable-expansion.patch @@ -0,0 +1,31 @@ +From 2478a55ac8af31f426f2d6bb5adf8fd2b3ac5eff Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 20 Sep 2023 20:36:49 +0000 +Subject: [PATCH 01/11] [conf] Add test for shell-like variable expansion + +Requires https://github.com/rpm-software-management/libdnf/pull/1622 + +This is the same test case used by the DNF 5 implementation: https://github.com/rpm-software-management/dnf5/pull/800 +--- + tests/conf/test_parser.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/tests/conf/test_parser.py b/tests/conf/test_parser.py +index a9e50460f..ad0d61e31 100644 +--- a/tests/conf/test_parser.py ++++ b/tests/conf/test_parser.py +@@ -54,6 +54,11 @@ class ParserTest(tests.support.TestCase): + result = '$Substitute some fact}withoutspace.' + self.assertEqual(substitute(rawstr, substs), result) + ++ # Test ${variable:-word} and ${variable:+word} shell-like expansion ++ rawstr = '${lies:+alternate}-${unset:-default}-${nn:+n${nn:-${nnn:}' ++ result = 'alternate-default-${nn:+n${nn:-${nnn:}' ++ self.assertEqual(substitute(rawstr, substs), result) ++ + def test_empty_option(self): + # Parser is able to read config file with option without value + FN = tests.support.resource_path('etc/empty_option.conf') +-- +2.49.0 + diff --git a/SOURCES/0058-Split-releasever-to-releasever_major-and-releasever_.patch b/SOURCES/0058-Split-releasever-to-releasever_major-and-releasever_.patch new file mode 100644 index 0000000..9f817ec --- /dev/null +++ b/SOURCES/0058-Split-releasever-to-releasever_major-and-releasever_.patch @@ -0,0 +1,139 @@ +From a614ec8eeb440f2fd4bfb196ddb1d24406d420a6 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 18 Sep 2023 20:42:09 +0000 +Subject: [PATCH 02/11] Split $releasever to $releasever_major and + $releasever_minor + +Whenever the `releasever` substitution variable is set, automatically +derive and set the `releasever_major` and `releasever_minor` vars by +splitting `releasever` on the first ".". + +Companion to the DNF 5 implementation here: https://github.com/rpm-software-management/dnf5/pull/800 + +DNF 5 issue: https://github.com/rpm-software-management/dnf5/issues/710 + +For https://bugzilla.redhat.com/show_bug.cgi?id=1789346 +--- + dnf/conf/substitutions.py | 31 +++++++++++++++++++++++++++++++ + dnf/exceptions.py | 6 ++++++ + tests/conf/test_substitutions.py | 32 ++++++++++++++++++++++++++++++++ + 3 files changed, 69 insertions(+) + +diff --git a/dnf/conf/substitutions.py b/dnf/conf/substitutions.py +index 4d0f0d55e..5c736a8df 100644 +--- a/dnf/conf/substitutions.py ++++ b/dnf/conf/substitutions.py +@@ -23,8 +23,10 @@ import os + import re + + 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")) + logger = logging.getLogger('dnf') + + +@@ -43,6 +45,35 @@ 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) ++ ++ setitem = super(Substitutions, self).__setitem__ ++ setitem(key, value) ++ ++ if key == "releasever" and value: ++ releasever_major, releasever_minor = Substitutions._split_releasever(value) ++ setitem("releasever_major", releasever_major) ++ setitem("releasever_minor", releasever_minor) ++ ++ @staticmethod ++ def is_read_only(key): ++ # type: (str) -> bool ++ return key in READ_ONLY_VARIABLES ++ + def update_from_etc(self, installroot, varsdir=("/etc/yum/vars/", "/etc/dnf/vars/")): + # :api + +diff --git a/dnf/exceptions.py b/dnf/exceptions.py +index ef731781d..2d009b2ad 100644 +--- a/dnf/exceptions.py ++++ b/dnf/exceptions.py +@@ -180,6 +180,12 @@ class ProcessLockError(LockError): + return (ProcessLockError, (self.value, self.pid)) + + ++class ReadOnlyVariableError(Error): ++ def __init__(self, value, variable_name): ++ super(ReadOnlyVariableError, self).__init__(value) ++ self.variable_name = variable_name ++ ++ + class RepoError(Error): + # :api + pass +diff --git a/tests/conf/test_substitutions.py b/tests/conf/test_substitutions.py +index b64533ff6..d8ac3c207 100644 +--- a/tests/conf/test_substitutions.py ++++ b/tests/conf/test_substitutions.py +@@ -23,6 +23,8 @@ from __future__ import unicode_literals + import os + + import dnf.conf ++from dnf.conf.substitutions import Substitutions ++from dnf.exceptions import ReadOnlyVariableError + + import tests.support + +@@ -52,3 +54,33 @@ class SubstitutionsFromEnvironmentTest(tests.support.TestCase): + conf.substitutions.keys(), + ['basearch', 'arch', 'GENRE', 'EMPTY']) + 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() ++ conf.substitutions["releasever"] = "1.23" ++ self.assertEqual(conf.substitutions["releasever_major"], "1") ++ self.assertEqual(conf.substitutions["releasever_minor"], "23") ++ ++ def test_releasever_major_only(self): ++ conf = dnf.conf.Conf() ++ conf.substitutions["releasever"] = "123" ++ self.assertEqual(conf.substitutions["releasever_major"], "123") ++ self.assertEqual(conf.substitutions["releasever_minor"], "") ++ ++ def test_releasever_multipart(self): ++ conf = dnf.conf.Conf() ++ conf.substitutions["releasever"] = "1.23.45" ++ self.assertEqual(conf.substitutions["releasever_major"], "1") ++ self.assertEqual(conf.substitutions["releasever_minor"], "23.45") +-- +2.49.0 + diff --git a/SOURCES/0059-Document-releasever_major-and-releasever_minor.patch b/SOURCES/0059-Document-releasever_major-and-releasever_minor.patch new file mode 100644 index 0000000..7aa2c9d --- /dev/null +++ b/SOURCES/0059-Document-releasever_major-and-releasever_minor.patch @@ -0,0 +1,49 @@ +From 8045771627933b20323457cf30108ea417834cdc Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 16 Oct 2023 18:27:02 +0000 +Subject: [PATCH 03/11] Document $releasever_major and $releasever_minor + +=changelog= +msg: Automatically derive $releasever_major and $releasever_minor from $releasever +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1789346 +--- + doc/conf_ref.rst | 15 +++++++++++++++ + 1 file changed, 15 insertions(+) + +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index 7ff286fee..9397f0008 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -488,6 +488,9 @@ configuration file by your distribution to override the DNF defaults. + :ref:`string ` + + Used for substitution of ``$releasever`` in the repository configuration. ++ ++ 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``. ++ + See also :ref:`repo variables `. + + .. _reposdir-label: +@@ -794,6 +797,18 @@ Right side of every repo option can be enriched by the following variables: + + Refers to the release version of operating system which DNF derives from information available in RPMDB. + ++.. _variable-releasever_major-label: ++ ++``$releasever_major`` ++ ++ Major version of ``$releasever``, i.e. the component of ``$releasever`` occurring before the first ``.``. ++ ++.. _variable-releasever_minor-label: ++ ++``$releasever_minor`` ++ ++ Minor version of ``$releasever``, i.e. the component of ``$releasever`` occurring after the first ``.``. ++ + .. _variable-user-defined-label: + + In addition to these hard coded variables, user-defined ones can also be used. They can be defined either via :ref:`variable files `, or by using special environmental variables. The names of these variables must be prefixed with DNF_VAR\_ and they can only consist of alphanumeric characters and underscores:: +-- +2.49.0 + diff --git a/SOURCES/0060-Document-shell-like-parameter-expansion-for-variable.patch b/SOURCES/0060-Document-shell-like-parameter-expansion-for-variable.patch new file mode 100644 index 0000000..eed2b4d --- /dev/null +++ b/SOURCES/0060-Document-shell-like-parameter-expansion-for-variable.patch @@ -0,0 +1,39 @@ +From 41843856d1f09c8fe718630a6fa4318441c7be38 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 16 Oct 2023 18:44:53 +0000 +Subject: [PATCH 04/11] Document shell-like parameter expansion for variables + +=changelog= +msg: Support ${parameter:-word} and ${parameter:+word} parameter expansion in variables +type: enhancement +resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1789346 +--- + doc/conf_ref.rst | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index 9397f0008..fdb34323c 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -829,6 +829,18 @@ Although users are encouraged to use named variables, the numbered environmental + [myrepo] + baseurl=https://example.site/pub/fedora/$DNF1/releases/$releasever + ++A limited form of shell-like parameter expansion is supported for variables. ++ ++``${my_variable:-word}`` If ``my_variable`` is unset or empty, then ``word`` will be substituted. Otherwise, the value of ``my_variable`` will be substituted. ++ ++``${my_variable:+word}`` If ``my_variable`` is set and not empty, then ``word`` will be substituted. Otherwise, the empty string will be substituted. ++ ++Parameter expansions can be nested up to a maximum depth of 32. For example:: ++ ++ ${my_defined_variable:+${my_undefined_variable:-foobar}} ++ ++will evaluate to ``foobar``. ++ + + .. _conf_main_and_repo_options-label: + +-- +2.49.0 + diff --git a/SOURCES/0061-Derive-releasever_-major-minor-in-conf-not-substitut.patch b/SOURCES/0061-Derive-releasever_-major-minor-in-conf-not-substitut.patch new file mode 100644 index 0000000..a919a05 --- /dev/null +++ b/SOURCES/0061-Derive-releasever_-major-minor-in-conf-not-substitut.patch @@ -0,0 +1,161 @@ +From 9a0f2a9ad87551900e7590ccf023fb9b72b3fe0c Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 20 Jan 2025 21:36:18 +0000 +Subject: [PATCH 05/11] Derive releasever_{major,minor} in conf, not + substitutions + +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 ed6daeb2d..49280e522 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -429,6 +429,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 5c736a8df..8582d5d84 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 d8ac3c207..78d3e7274 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 d85026705..16bdcccba 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.49.0 + diff --git a/SOURCES/0062-Override-releasever_-major-minor-with-provides.patch b/SOURCES/0062-Override-releasever_-major-minor-with-provides.patch new file mode 100644 index 0000000..694267b --- /dev/null +++ b/SOURCES/0062-Override-releasever_-major-minor-with-provides.patch @@ -0,0 +1,177 @@ +From 22d6966c80a83e932da8f7f47a907e4940ab1677 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 21 Jan 2025 19:16:13 +0000 +Subject: [PATCH 06/11] Override releasever_{major,minor} with provides + +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 d0ce6364c..7d3dfdee7 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 c41f31ed6..ca0e35c4d 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -994,13 +994,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 bcadc8041..07aab7a44 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 12efca7fb..d4be4d03a 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.49.0 + diff --git a/SOURCES/0063-Add-releasever-major-and-releasever-minor-options.patch b/SOURCES/0063-Add-releasever-major-and-releasever-minor-options.patch new file mode 100644 index 0000000..6d55255 --- /dev/null +++ b/SOURCES/0063-Add-releasever-major-and-releasever-minor-options.patch @@ -0,0 +1,122 @@ +From 8478b6314237f830418bf478f68ca06d6d3d9f48 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 24 Jan 2025 22:50:22 +0000 +Subject: [PATCH 07/11] Add --releasever-major and --releasever-minor options + +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 ca0e35c4d..21e8764d0 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -859,7 +859,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() +@@ -968,7 +968,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 + +@@ -996,18 +996,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 6bb32c517..cf52309d8 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -202,6 +202,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 30627bd4d..dc2e01efc 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -335,6 +335,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 fdb34323c..07ab6e27b 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -491,6 +491,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.49.0 + diff --git a/SOURCES/0064-doc-Document-detect_releasevers-and-update-example.patch b/SOURCES/0064-doc-Document-detect_releasevers-and-update-example.patch new file mode 100644 index 0000000..9d78db5 --- /dev/null +++ b/SOURCES/0064-doc-Document-detect_releasevers-and-update-example.patch @@ -0,0 +1,55 @@ +From c916d89d48e4579fd54f11971d24985e9ca3090a Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 27 Jan 2025 18:49:11 +0000 +Subject: [PATCH 08/11] doc: Document detect_releasevers and update example + +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 c59ed67d1..562be41a4 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 dbd3b8904..b1540e12e 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.49.0 + diff --git a/SOURCES/0065-tests-Patch-detect_releasevers-not-detect_releasever.patch b/SOURCES/0065-tests-Patch-detect_releasevers-not-detect_releasever.patch new file mode 100644 index 0000000..110b3fa --- /dev/null +++ b/SOURCES/0065-tests-Patch-detect_releasevers-not-detect_releasever.patch @@ -0,0 +1,128 @@ +From d0058b39d1d009856c85522908cb85ad59cf0ef2 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 27 Jan 2025 19:20:14 +0000 +Subject: [PATCH 09/11] tests: Patch detect_releasevers, not detect_releasever + +--- + 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 e6d8de847..fb606ffcd 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 cc0a5df30..c77cb3efe 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 e50684ef5..d03683edc 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 ad3ef6759..9e0a656d3 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 9c130c368..573d4ae2b 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.49.0 + diff --git a/SOURCES/0066-Document-how-releasever-releasever_-major-minor-affe.patch b/SOURCES/0066-Document-how-releasever-releasever_-major-minor-affe.patch new file mode 100644 index 0000000..2311c27 --- /dev/null +++ b/SOURCES/0066-Document-how-releasever-releasever_-major-minor-affe.patch @@ -0,0 +1,96 @@ +From c3ba8afd83f4df5a3ed66088a458d65926a03716 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 4 Feb 2025 23:01:43 +0000 +Subject: [PATCH 10/11] Document how --releasever, --releasever_{major,minor} + affect each other + +--- + 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 49280e522..3bcd2f3d3 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -424,6 +424,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 +@@ -437,6 +443,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 +@@ -445,6 +456,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 dc2e01efc..1a20ae397 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -338,10 +338,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 16bdcccba..69ba988c4 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.49.0 + diff --git a/SOURCES/0067-Move-releasever_minor-setter-docstring-to-the-correc.patch b/SOURCES/0067-Move-releasever_minor-setter-docstring-to-the-correc.patch new file mode 100644 index 0000000..cb09681 --- /dev/null +++ b/SOURCES/0067-Move-releasever_minor-setter-docstring-to-the-correc.patch @@ -0,0 +1,39 @@ +From 45429b40f588c739fab8e369d92fac3b78472b19 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Fri, 7 Feb 2025 14:22:09 -0500 +Subject: [PATCH 11/11] Move releasever_minor setter docstring to the correct + function + +--- + dnf/conf/config.py | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/dnf/conf/config.py b/dnf/conf/config.py +index 3bcd2f3d3..b6a1b0f2f 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -456,16 +456,16 @@ 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 + def releasever_minor(self, val): + # :api ++ """ ++ Override the releasever_minor 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_minor', None) + return +-- +2.49.0 + diff --git a/SPECS/dnf.spec b/SPECS/dnf.spec index c55f4c3..68be14e 100644 --- a/SPECS/dnf.spec +++ b/SPECS/dnf.spec @@ -2,7 +2,7 @@ %define __cmake_in_source_build 1 # default dependencies -%global hawkey_version 0.74.0 +%global hawkey_version 0.75.0 %global libcomps_version 0.1.8 %global libmodulemd_version 2.9.3 %global rpm_version 4.14.0 @@ -22,7 +22,7 @@ %endif %if 0%{?rhel} == 9 - %global hawkey_version 0.69.0-13 + %global hawkey_version 0.69.0-16 %endif # override dependencies for fedora 26 @@ -73,7 +73,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.14.0 -Release: 25%{?dist}.alma.1 +Release: 31%{?dist}.alma.1 Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPLv2+ @@ -123,6 +123,29 @@ Patch41: 0041-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch Patch42: 0042-bootc-Re-locking-use-ostree-admin-unlock-transient.patch Patch43: 0043-spec-Add-dnf-bootc-subpackage.patch Patch44: 0044-Require-libdnf-0.74.0-with-persistence-option.patch +Patch45: 0045-package-remote_location-takes-basedir-into-account.patch +Patch46: 0046-Usage-help-don-t-mark-mandatory-option-parameters-as.patch +Patch47: 0047-Fix-typo-from-previous-commit-left-over.patch +Patch48: 0048-disableexcludes-and-disableexcludepkgs-values-are-no.patch +Patch49: 0049-bootc-tmt-testing.patch +Patch50: 0050-persistence-store-persist-transient-in-history-DB.patch +Patch51: 0051-Print-persist-or-transient-in-history-info.patch +Patch52: 0052-history-persistence-for-MergedTransaction.patch +Patch53: 0053-bootc-Check-whether-protected-paths-will-be-modified.patch +Patch54: 0054-spec-package-etc-dnf-usr_drift_protected_paths.d.patch +Patch55: 0055-Support-globs-in-usr_drift_protected_paths.patch +Patch56: 0056-doc-Document-usr_drift_protected_paths.patch +Patch57: 0057-conf-Add-test-for-shell-like-variable-expansion.patch +Patch58: 0058-Split-releasever-to-releasever_major-and-releasever_.patch +Patch59: 0059-Document-releasever_major-and-releasever_minor.patch +Patch60: 0060-Document-shell-like-parameter-expansion-for-variable.patch +Patch61: 0061-Derive-releasever_-major-minor-in-conf-not-substitut.patch +Patch62: 0062-Override-releasever_-major-minor-with-provides.patch +Patch63: 0063-Add-releasever-major-and-releasever-minor-options.patch +Patch64: 0064-doc-Document-detect_releasevers-and-update-example.patch +Patch65: 0065-tests-Patch-detect_releasevers-not-detect_releasever.patch +Patch66: 0066-Document-how-releasever-releasever_-major-minor-affe.patch +Patch67: 0067-Move-releasever_minor-setter-docstring-to-the-correc.patch # AlmaLinux Patch Patch10000: almalinux_bugtracker.patch @@ -222,6 +245,7 @@ Requires: rpm-plugin-systemd-inhibit %else Recommends: (rpm-plugin-systemd-inhibit if systemd) %endif +Provides: dnf4 = %{version}-%{release} %description -n python3-%{name} Python 3 interface to DNF. @@ -277,6 +301,7 @@ mkdir -p %{buildroot}%{_localstatedir}/log/ mkdir -p %{buildroot}%{_var}/cache/dnf/ touch %{buildroot}%{_localstatedir}/log/%{name}.log ln -sr %{buildroot}%{_bindir}/dnf-3 %{buildroot}%{_bindir}/dnf +ln -sr %{buildroot}%{_bindir}/dnf-3 %{buildroot}%{_bindir}/dnf4 mv %{buildroot}%{_bindir}/dnf-automatic-3 %{buildroot}%{_bindir}/dnf-automatic rm -vf %{buildroot}%{_bindir}/dnf-automatic-* @@ -350,6 +375,7 @@ popd %dir %{confdir}/modules.defaults.d %dir %{pluginconfpath} %dir %{confdir}/protected.d +%dir %{confdir}/usr-drift-protected-paths.d %dir %{confdir}/vars %dir %{confdir}/aliases.d %exclude %{confdir}/aliases.d/zypper.conf @@ -405,6 +431,7 @@ popd %files -n python3-%{name} %{_bindir}/%{name}-3 +%{_bindir}/%{name}4 %exclude %{python3_sitelib}/%{name}/automatic %{python3_sitelib}/%{name}/ %dir %{py3pluginpath} @@ -428,9 +455,29 @@ popd # bootc subpackage does not include any files %changelog -* Tue May 13 2025 Eduard Abdullin - 4.14.0-25.alma.1 +* Tue Nov 11 2025 Eduard Abdullin - 4.14.0-31.alma.1 - Added patch for almalinux bugtracker +* Mon Jun 30 2025 Evan Goode - 4.14.0-31 +- Introduce $releasever_major, $releasever_minor variables, shell-style + variable substitution (RHEL-65817) + +* Thu Jun 26 2025 Evan Goode - 4.14.0-30 +- Mark transient transactions in DNF history (RHEL-84512) +- Warn/disallow changes outside /usr, /etc with --transient (RHEL-84499) + +* Fri May 02 2025 Ales Matej - 4.14.0-29 +- man page: don't mark mandatory option parameters as optional (RHEL-63958) + +* Fri Apr 04 2025 Evan Goode - 4.14.0-28 +- Add dnf4 provides and symlink /usr/bin/dnf4 -> /usr/bin/dnf-3 (RHEL-82310) + +* Fri Mar 07 2025 Ales Matej - 4.14.0-27 +- usage help: don't mark mandatory option parameters as optional (RHEL-63958) + +* Fri Mar 07 2025 Marek Blaha - 4.14.0-26 +- package: remote_location() takes basedir into account (RHEL-71125) + * Tue Feb 04 2025 Petr Pisar - 4.14.0-25 - Add support for transient transactions (RHEL-70917)