blob: d22bdd0ba4f0e7b3949bfa63638b77ec01ac6948 [file]
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022-2023 Intel Corporation
"""Convert Cargo versions into Meson compatible ones."""
from __future__ import annotations
from functools import lru_cache
import operator
import re
import typing as T
from ..mesonlib import MesonException
def _api_of(version: str) -> str:
# x.y.z -> x
# 0.x.y -> 0.x
# 0.0.x -> 0
vers = version.split('.')
if not vers[0] or int(vers[0]) != 0:
return vers[0]
elif len(vers) >= 2 and int(vers[1]) != 0:
return f'0.{vers[1]}'
return '0'
def split(cargo_ver: str) -> T.Iterable[tuple[str, str]]:
"""Canonicalization of Cargo version requirements"""
cargo_ver = cargo_ver.strip()
if not cargo_ver:
return
for ver in cargo_ver.split(','):
ver = ver.strip()
if ver == '*':
continue
if ver.startswith(('>=', '<=', '!=')):
yield ver[0:2], ver[2:].lstrip()
elif ver.startswith(('~', '=', '^', '>', '<')):
yield ver[0], ver[1:].lstrip()
elif ver.endswith('.*'):
# asterisk requirements are same as tilde: 1.* == ~1
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#wildcard-requirements
yield '~', ver[:-2].lstrip()
else:
# caret requirement is the default strategy
yield '^', ver
def api(cargo_ver: str) -> str:
"""Determine the API version implied by a Cargo version requirement.
:param cargo_ver: A Cargo version string.
:return: empty string if no lower bound establishes an API, otherwise the
major version (or ``"0.x"`` / ``"0"`` for 0.x.y / 0.0.x versions).
Raise exception if constraints disagree on the API.
"""
apis: T.Set[str] = set()
for op, ver in split(cargo_ver):
if op in {'>=', '=', '^', '~'}:
apis.add(_api_of(ver))
if not apis:
return ''
elif len(apis) == 1:
return apis.pop()
else:
raise MesonException(f'Cannot determine API version from {cargo_ver!r}.')
# Tokens: a digit run, an alphanumeric-with-hyphens identifier (covers the
# leading ``-`` of the pre-release section and identifiers within it), or the
# ``+`` that introduces build metadata.
_SEMVER_TOK_RE = re.compile(r'(\d+)|([A-Za-z-][0-9A-Za-z-]*)|(\+.*)')
class SemVer:
"""A SemVer 2.0.0 version, suitable for ordering.
Adapted from ``mesonlib.Version`` but with the int/str precedence
reversed: SemVer says numeric pre-release identifiers sort *below*
alphanumeric ones, whereas Meson's ``Version`` does the opposite.
Versions are represented as a flat list:
[major, minor, patch, 0] for normal versions
[major, minor, patch, -1, *pre_idents] for pre-releases
The fourth slot (0 for normal, -1 for pre-release) makes a normal
version sort *above* the corresponding pre-release.
Missing minor/patch components default to 0.
"""
__slots__ = ('_v', 'specified_count')
def __init__(self, in_: str | list[int | str] = None) -> None:
vec: list[int | str]
if isinstance(in_, str):
vec = []
pre = False
specified_count = 0
for m in _SEMVER_TOK_RE.finditer(in_):
if m.group(1):
if pre or specified_count < 3:
vec.append(int(m.group(1)))
if not pre:
specified_count += 1
elif m.group(2):
ident = m.group(2)
if not pre:
# The leading ``-`` is just a section marker.
if ident.startswith('-'):
ident = ident[1:]
if not ident:
continue
while len(vec) < 3:
vec.append(0)
vec.append(-1)
pre = True
vec.append(ident)
else:
break # +build metadata: discard the rest
else:
# Direct construction from a pre-built component list.
vec = list(in_)
specified_count = min(3, len(in_))
while len(vec) < 4:
vec.append(0)
self._v = vec
self.specified_count = specified_count
def __repr__(self) -> str:
s = '.'.join(str(c) for c in self._v[:self.specified_count])
if len(self._v) > 4:
s += '-' + '.'.join(str(c) for c in self._v[4:])
return f'<SemVer: {s}>'
@property
def has_prerelease(self) -> bool:
return self._v[3] == -1
def __lt__(self, other: object) -> bool:
if isinstance(other, SemVer):
return self.__cmp(other._v, operator.lt)
return NotImplemented
def __gt__(self, other: object) -> bool:
if isinstance(other, SemVer):
return self.__cmp(other._v, operator.gt)
return NotImplemented
def __le__(self, other: object) -> bool:
if isinstance(other, SemVer):
return self.__cmp(other._v, operator.le)
return NotImplemented
def __ge__(self, other: object) -> bool:
if isinstance(other, SemVer):
return self.__cmp(other._v, operator.ge)
return NotImplemented
def __eq__(self, other: object) -> bool:
if isinstance(other, SemVer):
return self._v == other._v
return NotImplemented
def __ne__(self, other: object) -> bool:
if isinstance(other, SemVer):
return self._v != other._v
return NotImplemented
def __cmp(self, other: list[int | str], comparator: T.Callable[[T.Any, T.Any], bool]) -> bool:
for ours, theirs in zip(self._v, other):
ours_is_int = isinstance(ours, int)
theirs_is_int = isinstance(theirs, int)
if ours_is_int != theirs_is_int:
# SemVer: int (numeric) < str (alphanumeric).
return comparator(theirs_is_int, ours_is_int)
if ours != theirs:
return comparator(ours, theirs)
# "A larger set of pre-release fields has a higher precedence."
return comparator(len(self._v), len(other))
def next_ver(self, bump_idx: int) -> SemVer:
"""Return a new SemVer with the component at ``bump_idx`` (0=major,
1=minor, 2=patch) bumped, following normal-version components zeroed,
and any pre-release dropped."""
v = list(self._v[:3])
last = v[bump_idx]
assert isinstance(last, int)
v[bump_idx] = last + 1
for i in range(bump_idx + 1, 3):
v[i] = 0
return SemVer(v)
@lru_cache(maxsize=None)
def cargo_parse(cargo_ver: str) -> T.Callable[[str], bool]:
"""Return a function that checks a Version against a Cargo version
requirement.
:param cargo_ver: The version, as Cargo specifies
:return: A function returning true if the version is accepted.
"""
out: list[tuple[T.Callable[[T.Any, T.Any], bool], SemVer]] = []
# Pre-release versions only match when at least one constraint
# names a pre-release.
accept_prerelease = False
for op, ver in split(cargo_ver):
semver = SemVer(ver)
accept_prerelease = accept_prerelease or semver.has_prerelease
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#comparison-requirements
if op == '<=':
# Bump the last *specified* component and convert to `<`.
nextver = semver.next_ver(semver.specified_count - 1)
out.append((operator.lt, nextver))
elif op == '~':
# Tilde requirements are the same as asterisk, so 1.* == ~1
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#tilde-requirements
# we convert those into a pair of constraints
out.append((operator.ge, semver))
if semver.specified_count >= 2:
nextver = semver.next_ver(1)
else:
nextver = semver.next_ver(0)
out.append((operator.lt, nextver))
elif op == '^':
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#caret-requirements
# Bump the leftmost non-zero major/minor/patch component
out.append((operator.ge, semver))
for bump_idx in range(3):
if semver._v[bump_idx] != 0:
break
else:
# All zeros: ``^0.0.0`` means ``< 1.0.0``, so bump the major.
bump_idx = 0
nextver = semver.next_ver(bump_idx)
out.append((operator.lt, nextver))
elif op == '>=':
out.append((operator.ge, semver))
elif op == '!=':
out.append((operator.ne, semver))
elif op == '=':
out.append((operator.eq, semver))
elif op == '>':
out.append((operator.gt, semver))
elif op == '<':
out.append((operator.lt, semver))
def compare(ver: str) -> bool:
lhs = SemVer(ver)
if lhs.has_prerelease and not accept_prerelease:
return False
for op, rhs in out:
if not op(lhs, rhs):
return False
return True
if not out:
return lambda v: True
return compare