Split $releasever_{major,minor}, shell-style variable expansion

Resolves: RHEL-65817
This commit is contained in:
Evan Goode 2025-06-30 21:27:10 +00:00
parent 54c319b6fd
commit bac4aa45b5
12 changed files with 1053 additions and 2 deletions

View File

@ -0,0 +1,31 @@
From 2478a55ac8af31f426f2d6bb5adf8fd2b3ac5eff Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,139 @@
From a614ec8eeb440f2fd4bfb196ddb1d24406d420a6 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,49 @@
From 8045771627933b20323457cf30108ea417834cdc Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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 <string-label>`
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 <repo-variables-label>`.
.. _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 <varfiles-label>`, 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

View File

@ -0,0 +1,39 @@
From 41843856d1f09c8fe718630a6fa4318441c7be38 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,161 @@
From 9a0f2a9ad87551900e7590ccf023fb9b72b3fe0c Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,177 @@
From 22d6966c80a83e932da8f7f47a907e4940ab1677 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,122 @@
From 8478b6314237f830418bf478f68ca06d6d3d9f48 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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 ``<release>``. This can
affect cache paths, values in configuration files and mirrorlist URLs.
+``--releasever_major=<major version>``
+ Override the releasever_major variable, which is usually automatically
+ detected or taken from the part of ``$releasever`` before the first ``.``.
+
+``--releasever_minor=<minor version>``
+ 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 <repo-variables-label>`.
.. _reposdir-label:
--
2.49.0

View File

@ -0,0 +1,55 @@
From c916d89d48e4579fd54f11971d24985e9ca3090a Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,128 @@
From d0058b39d1d009856c85522908cb85ad59cf0ef2 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -0,0 +1,96 @@
From c3ba8afd83f4df5a3ed66088a458d65926a03716 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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=<major version>``
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=<minor version>``
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

View File

@ -0,0 +1,39 @@
From 45429b40f588c739fab8e369d92fac3b78472b19 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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

View File

@ -22,7 +22,7 @@
%endif
%if 0%{?rhel} == 9
%global hawkey_version 0.69.0-15
%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: 30%{?dist}
Release: 31%{?dist}
Summary: %{pkg_summary}
# For a breakdown of the licensing, see PACKAGE-LICENSING
License: GPLv2+
@ -135,6 +135,17 @@ 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
BuildArch: noarch
BuildRequires: cmake
@ -441,6 +452,10 @@ popd
# bootc subpackage does not include any files
%changelog
* Mon Jun 30 2025 Evan Goode <egoode@redhat.com> - 4.14.0-31
- Introduce $releasever_major, $releasever_minor variables, shell-style
variable substitution (RHEL-65817)
* Thu Jun 26 2025 Evan Goode <egoode@redhat.com> - 4.14.0-30
- Mark transient transactions in DNF history (RHEL-84512)
- Warn/disallow changes outside /usr, /etc with --transient (RHEL-84499)