|
2 | 2 | Common functions for working with RPM packages
|
3 | 3 | """
|
4 | 4 |
|
| 5 | +from __future__ import annotations |
| 6 | + |
5 | 7 | import collections
|
6 | 8 | import datetime
|
7 | 9 | import logging
|
@@ -157,6 +159,156 @@ def combine_comments(comments):
|
157 | 159 | return "".join(ret)
|
158 | 160 |
|
159 | 161 |
|
| 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 | + |
160 | 312 | def version_to_evr(verstring):
|
161 | 313 | """
|
162 | 314 | Split the package version string into epoch, version and release.
|
|
0 commit comments