Skip to content

Commit d8f5def

Browse files
committed
Fix rpm pkg warnings and remove fallback to slow process fork
1 parent b539903 commit d8f5def

File tree

5 files changed

+680
-205
lines changed

5 files changed

+680
-205
lines changed

changelog/68341.fixed.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed pkg.installed state from showing warning if python rpm package not installed.
2+
Fixed pkg.installed state from showing warning and using slow process fork for version comparison when rpmdevtools is installed

salt/modules/rpm_lowpkg.py

Lines changed: 33 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Support for rpm
33
"""
44

5+
from __future__ import annotations
6+
57
import datetime
68
import logging
79
import os
@@ -11,7 +13,6 @@
1113
import salt.utils.itertools
1214
import salt.utils.path
1315
import salt.utils.pkg.rpm
14-
import salt.utils.versions
1516
from salt.exceptions import CommandExecutionError, SaltInvocationError
1617
from salt.utils.versions import LooseVersion
1718

@@ -698,14 +699,23 @@ def version_cmp(ver1, ver2, ignore_epoch=False):
698699
"""
699700

700701
def normalize(x):
701-
return str(x).split(":", 1)[-1] if ignore_epoch else str(x)
702+
return str(x).split(":", maxsplit=1)[-1] if ignore_epoch else str(x)
702703

703704
ver1 = normalize(ver1)
704705
ver2 = normalize(ver2)
705706

706-
try:
707-
cmp_func = None
708-
if HAS_RPM:
707+
(ver1_e, ver1_v, ver1_r) = salt.utils.pkg.rpm.version_to_evr(ver1)
708+
(ver2_e, ver2_v, ver2_r) = salt.utils.pkg.rpm.version_to_evr(ver2)
709+
# If one EVR is missing a release but not the other and they
710+
# otherwise would be equal, ignore the release. This can happen if
711+
# e.g. you are checking if a package version 3.2 is satisfied by
712+
# 3.2-1.
713+
if not ver1_r or not ver2_r:
714+
ver1_r = ver2_r = ""
715+
716+
if HAS_RPM:
717+
try:
718+
cmp_func = None
709719
try:
710720
cmp_func = rpm.labelCompare
711721
except AttributeError:
@@ -716,91 +726,27 @@ def normalize(x):
716726
"labelCompare function. Not using rpm.labelCompare for "
717727
"version comparison."
718728
)
719-
else:
720-
log.warning(
721-
"Please install a package that provides rpm.labelCompare for "
722-
"more accurate version comparisons."
723-
)
724-
725-
# If one EVR is missing a release but not the other and they
726-
# otherwise would be equal, ignore the release. This can happen if
727-
# e.g. you are checking if a package version 3.2 is satisfied by
728-
# 3.2-1.
729-
(ver1_e, ver1_v, ver1_r) = salt.utils.pkg.rpm.version_to_evr(ver1)
730-
(ver2_e, ver2_v, ver2_r) = salt.utils.pkg.rpm.version_to_evr(ver2)
731-
732-
if not ver1_r or not ver2_r:
733-
ver1_r = ver2_r = ""
734-
735-
if cmp_func is None:
736-
ver1 = f"{ver1_e}:{ver1_v}-{ver1_r}"
737-
ver2 = f"{ver2_e}:{ver2_v}-{ver2_r}"
738-
739-
if salt.utils.path.which("rpmdev-vercmp"):
740-
log.warning(
741-
"Installing the rpmdevtools package may surface dev tools in"
742-
" production."
729+
if cmp_func is not None:
730+
cmp_result = cmp_func(
731+
(ver1_e, ver1_v, ver1_r), (ver2_e, ver2_v, ver2_r)
743732
)
744-
745-
# rpmdev-vercmp always uses epochs, even when zero
746-
def _ensure_epoch(ver):
747-
def _prepend(ver):
748-
return f"0:{ver}"
749-
750-
try:
751-
if ":" not in ver:
752-
return _prepend(ver)
753-
except TypeError:
754-
return _prepend(ver)
755-
return ver
756-
757-
ver1 = _ensure_epoch(ver1)
758-
ver2 = _ensure_epoch(ver2)
759-
result = __salt__["cmd.run_all"](
760-
["rpmdev-vercmp", ver1, ver2],
761-
python_shell=False,
762-
redirect_stderr=True,
763-
ignore_retcode=True,
764-
)
765-
# rpmdev-vercmp returns 0 on equal, 11 on greater-than, and
766-
# 12 on less-than.
767-
if result["retcode"] == 0:
768-
return 0
769-
elif result["retcode"] == 11:
770-
return 1
771-
elif result["retcode"] == 12:
772-
return -1
773-
else:
774-
# We'll need to fall back to salt.utils.versions.version_cmp()
775-
log.warning(
776-
"Failed to interpret results of rpmdev-vercmp output. "
777-
"This is probably a bug, and should be reported. "
778-
"Return code was %s. Output: %s",
779-
result["retcode"],
780-
result["stdout"],
733+
if cmp_result not in (-1, 0, 1):
734+
raise CommandExecutionError(
735+
f"Comparison result '{cmp_result}' is invalid"
781736
)
782-
else:
783-
log.warning(
784-
"Falling back on salt.utils.versions.version_cmp() for version"
785-
" comparisons"
786-
)
787-
else:
788-
cmp_result = cmp_func((ver1_e, ver1_v, ver1_r), (ver2_e, ver2_v, ver2_r))
789-
if cmp_result not in (-1, 0, 1):
790-
raise CommandExecutionError(
791-
f"Comparison result '{cmp_result}' is invalid"
792-
)
793-
return cmp_result
794-
795-
except Exception as exc: # pylint: disable=broad-except
796-
log.warning(
797-
"Failed to compare version '%s' to '%s' using RPM: %s", ver1, ver2, exc
737+
return cmp_result
738+
except Exception as exc: # pylint: disable=broad-except
739+
log.warning(
740+
"Failed to compare version '%s' to '%s' using RPM: %s", ver1, ver2, exc
741+
)
742+
else:
743+
log.info(
744+
"Install a package that provides rpm.labelCompare for "
745+
"faster version comparisons."
798746
)
799-
800-
# We would already have normalized the versions at the beginning of this
801-
# function if ignore_epoch=True, so avoid unnecessary work and just pass
802-
# False for this value.
803-
return salt.utils.versions.version_cmp(ver1, ver2, ignore_epoch=False)
747+
return salt.utils.pkg.rpm.evr_compare(
748+
(ver1_e, ver1_v, ver1_r), (ver2_e, ver2_v, ver2_r)
749+
)
804750

805751

806752
def checksum(*paths, **kwargs):

salt/utils/pkg/rpm.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Common functions for working with RPM packages
33
"""
44

5+
from __future__ import annotations
6+
57
import collections
68
import datetime
79
import logging
@@ -157,6 +159,156 @@ def combine_comments(comments):
157159
return "".join(ret)
158160

159161

162+
def evr_compare(
163+
evr1: tuple[str | None, str | None, str | None],
164+
evr2: tuple[str | None, str | None, str | None],
165+
) -> int:
166+
"""
167+
Compare two RPM package identifiers using full epoch–version–release semantics.
168+
169+
This is a pure‑Python equivalent of ``rpm.labelCompare()``, returning the same
170+
ordering as the system RPM library without requiring the ``python3-rpm`` bindings.
171+
172+
The comparison is performed in three stages:
173+
174+
1. **Epoch** — compared numerically; missing or empty values are treated as 0.
175+
2. **Version** — compared using RPM's ``rpmvercmp`` rules:
176+
- Split into digit, alpha, and tilde (``~``) segments.
177+
- Tilde sorts before all other characters (e.g. ``1.0~beta`` < ``1.0``).
178+
- Numeric segments are compared as integers, ignoring leading zeros.
179+
- Numeric segments sort before alpha segments.
180+
3. **Release** — compared with the same rules as version.
181+
182+
:param evr1: The first ``(epoch, version, release)`` triple to compare.
183+
Each element may be a string or ``None``.
184+
:param evr2: The second ``(epoch, version, release)`` triple to compare.
185+
Each element may be a string or ``None``.
186+
:return: ``-1`` if ``evr1`` is considered older than ``evr2``,
187+
``0`` if they are considered equal,
188+
``1`` if ``evr1`` is considered newer than ``evr2``.
189+
190+
.. note::
191+
This comparison is **not** the same as PEP 440, ``LooseVersion``, or
192+
``StrictVersion``. It is intended for RPM package metadata and will match
193+
the ordering used by tools like ``rpm``, ``dnf``, and ``yum``.
194+
195+
.. code-block:: python
196+
197+
>>> label_compare(("0", "1.2.3", "1"), ("0", "1.2.3", "2"))
198+
-1
199+
>>> label_compare(("1", "1.0", "1"), ("0", "9.9", "9"))
200+
1
201+
>>> label_compare(("0", "1.0~beta", "1"), ("0", "1.0", "1"))
202+
-1
203+
"""
204+
epoch1, version1, release1 = evr1
205+
epoch2, version2, release2 = evr2
206+
epoch1 = int(epoch1 or 0)
207+
epoch2 = int(epoch2 or 0)
208+
if epoch1 != epoch2:
209+
return 1 if epoch1 > epoch2 else -1
210+
cmp_versions = _rpmvercmp(version1 or "", version2 or "")
211+
if cmp_versions != 0:
212+
return cmp_versions
213+
return _rpmvercmp(release1 or "", release2 or "")
214+
215+
216+
def _rpmvercmp(a: str, b: str) -> int:
217+
"""
218+
Pure-Python comparator matching RPM's rpmvercmp().
219+
Handles separators, tilde (~), caret (^), numeric/alpha segments.
220+
"""
221+
# Fast path: identical strings
222+
if a == b:
223+
return 0
224+
225+
# Work with mutable indices instead of C char* pointers
226+
i = j = 0
227+
la, lb = len(a), len(b)
228+
229+
def isalnum_(c: str) -> bool:
230+
return c.isalnum()
231+
232+
while i < la or j < lb:
233+
# Skip separators: anything not alnum, not ~, not ^
234+
while i < la and not (isalnum_(a[i]) or a[i] in "~^"):
235+
i += 1
236+
while j < lb and not (isalnum_(b[j]) or b[j] in "~^"):
237+
j += 1
238+
239+
# Tilde: sorts before everything else
240+
if i < la and a[i] == "~" or j < lb and b[j] == "~":
241+
if not (i < la and a[i] == "~"):
242+
return 1
243+
if not (j < lb and b[j] == "~"):
244+
return -1
245+
i += 1
246+
j += 1
247+
continue
248+
249+
# Caret: like tilde except base (end) loses to caret
250+
if i < la and a[i] == "^" or j < lb and b[j] == "^":
251+
if i >= la:
252+
return -1
253+
if j >= lb:
254+
return 1
255+
if not (i < la and a[i] == "^"):
256+
return 1
257+
if not (j < lb and b[j] == "^"):
258+
return -1
259+
i += 1
260+
j += 1
261+
continue
262+
263+
# If either ran out now, stop
264+
if not (i < la and j < lb):
265+
break
266+
267+
# Segment start positions
268+
si, sj = i, j
269+
270+
# Decide type from left side
271+
isnum = a[i].isdigit()
272+
if isnum:
273+
while i < la and a[i].isdigit():
274+
i += 1
275+
while j < lb and b[j].isdigit():
276+
j += 1
277+
else:
278+
while i < la and a[i].isalpha():
279+
i += 1
280+
while j < lb and b[j].isalpha():
281+
j += 1
282+
283+
# If right side had no same‑type run, types differ
284+
if sj == j:
285+
return 1 if isnum else -1
286+
287+
seg_a = a[si:i]
288+
seg_b = b[sj:j]
289+
290+
if isnum:
291+
# Strip leading zeros
292+
seg_a_nz = seg_a.lstrip("0")
293+
seg_b_nz = seg_b.lstrip("0")
294+
# Compare by length
295+
if len(seg_a_nz) != len(seg_b_nz):
296+
return 1 if len(seg_a_nz) > len(seg_b_nz) else -1
297+
# Same length: lexicographic
298+
if seg_a_nz != seg_b_nz:
299+
return 1 if seg_a_nz > seg_b_nz else -1
300+
else:
301+
# Alpha vs alpha
302+
if seg_a != seg_b:
303+
return 1 if seg_a > seg_b else -1
304+
# else equal segment → loop continues
305+
306+
# Tail handling
307+
if i >= la and j >= lb:
308+
return 0
309+
return -1 if i >= la else 1
310+
311+
160312
def version_to_evr(verstring):
161313
"""
162314
Split the package version string into epoch, version and release.

0 commit comments

Comments
 (0)