summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--semver.py1420
-rw-r--r--talerbuildconfig.py19
-rw-r--r--testconfigure.py2
3 files changed, 1438 insertions, 3 deletions
diff --git a/semver.py b/semver.py
new file mode 100644
index 0000000..7fd871e
--- /dev/null
+++ b/semver.py
@@ -0,0 +1,1420 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) The python-semanticversion project
+# This code is distributed under the two-clause BSD License.
+
+import functools
+import re
+import warnings
+
+
+def _has_leading_zero(value):
+ return (value
+ and value[0] == '0'
+ and value.isdigit()
+ and value != '0')
+
+
+class MaxIdentifier(object):
+ __slots__ = []
+
+ def __repr__(self):
+ return 'MaxIdentifier()'
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+
+@functools.total_ordering
+class NumericIdentifier(object):
+ __slots__ = ['value']
+
+ def __init__(self, value):
+ self.value = int(value)
+
+ def __repr__(self):
+ return 'NumericIdentifier(%r)' % self.value
+
+ def __eq__(self, other):
+ if isinstance(other, NumericIdentifier):
+ return self.value == other.value
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, MaxIdentifier):
+ return True
+ elif isinstance(other, AlphaIdentifier):
+ return True
+ elif isinstance(other, NumericIdentifier):
+ return self.value < other.value
+ else:
+ return NotImplemented
+
+
+@functools.total_ordering
+class AlphaIdentifier(object):
+ __slots__ = ['value']
+
+ def __init__(self, value):
+ self.value = value.encode('ascii')
+
+ def __repr__(self):
+ return 'AlphaIdentifier(%r)' % self.value
+
+ def __eq__(self, other):
+ if isinstance(other, AlphaIdentifier):
+ return self.value == other.value
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, MaxIdentifier):
+ return True
+ elif isinstance(other, NumericIdentifier):
+ return False
+ elif isinstance(other, AlphaIdentifier):
+ return self.value < other.value
+ else:
+ return NotImplemented
+
+
+class Version(object):
+
+ version_re = re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$')
+ partial_version_re = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-([0-9a-zA-Z.-]*))?(?:\+([0-9a-zA-Z.-]*))?$')
+
+ def __init__(
+ self,
+ version_string=None,
+ major=None,
+ minor=None,
+ patch=None,
+ prerelease=None,
+ build=None,
+ partial=False):
+ if partial:
+ warnings.warn(
+ "Partial versions will be removed in 3.0; use SimpleSpec('1.x.x') instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ has_text = version_string is not None
+ has_parts = not (major is minor is patch is prerelease is build is None)
+ if not has_text ^ has_parts:
+ raise ValueError("Call either Version('1.2.3') or Version(major=1, ...).")
+
+ if has_text:
+ major, minor, patch, prerelease, build = self.parse(version_string, partial)
+ else:
+ # Convenience: allow to omit prerelease/build.
+ prerelease = tuple(prerelease or ())
+ if not partial:
+ build = tuple(build or ())
+ self._validate_kwargs(major, minor, patch, prerelease, build, partial)
+
+ self.major = major
+ self.minor = minor
+ self.patch = patch
+ self.prerelease = prerelease
+ self.build = build
+
+ self.partial = partial
+
+ @classmethod
+ def _coerce(cls, value, allow_none=False):
+ if value is None and allow_none:
+ return value
+ return int(value)
+
+ def next_major(self):
+ if self.prerelease and self.minor == self.patch == 0:
+ return Version(
+ major=self.major,
+ minor=0,
+ patch=0,
+ partial=self.partial,
+ )
+ else:
+ return Version(
+ major=self.major + 1,
+ minor=0,
+ patch=0,
+ partial=self.partial,
+ )
+
+ def next_minor(self):
+ if self.prerelease and self.patch == 0:
+ return Version(
+ major=self.major,
+ minor=self.minor,
+ patch=0,
+ partial=self.partial,
+ )
+ else:
+ return Version(
+ major=self.major,
+ minor=self.minor + 1,
+ patch=0,
+ partial=self.partial,
+ )
+
+ def next_patch(self):
+ if self.prerelease:
+ return Version(
+ major=self.major,
+ minor=self.minor,
+ patch=self.patch,
+ partial=self.partial,
+ )
+ else:
+ return Version(
+ major=self.major,
+ minor=self.minor,
+ patch=self.patch + 1,
+ partial=self.partial,
+ )
+
+ def truncate(self, level='patch'):
+ """Return a new Version object, truncated up to the selected level."""
+ if level == 'build':
+ return self
+ elif level == 'prerelease':
+ return Version(
+ major=self.major,
+ minor=self.minor,
+ patch=self.patch,
+ prerelease=self.prerelease,
+ partial=self.partial,
+ )
+ elif level == 'patch':
+ return Version(
+ major=self.major,
+ minor=self.minor,
+ patch=self.patch,
+ partial=self.partial,
+ )
+ elif level == 'minor':
+ return Version(
+ major=self.major,
+ minor=self.minor,
+ patch=None if self.partial else 0,
+ partial=self.partial,
+ )
+ elif level == 'major':
+ return Version(
+ major=self.major,
+ minor=None if self.partial else 0,
+ patch=None if self.partial else 0,
+ partial=self.partial,
+ )
+ else:
+ raise ValueError("Invalid truncation level `%s`." % level)
+
+ @classmethod
+ def coerce(cls, version_string, partial=False):
+ """Coerce an arbitrary version string into a semver-compatible one.
+
+ The rule is:
+ - If not enough components, fill minor/patch with zeroes; unless
+ partial=True
+ - If more than 3 dot-separated components, extra components are "build"
+ data. If some "build" data already appeared, append it to the
+ extra components
+
+ Examples:
+ >>> Version.coerce('0.1')
+ Version(0, 1, 0)
+ >>> Version.coerce('0.1.2.3')
+ Version(0, 1, 2, (), ('3',))
+ >>> Version.coerce('0.1.2.3+4')
+ Version(0, 1, 2, (), ('3', '4'))
+ >>> Version.coerce('0.1+2-3+4_5')
+ Version(0, 1, 0, (), ('2-3', '4-5'))
+ """
+ base_re = re.compile(r'^\d+(?:\.\d+(?:\.\d+)?)?')
+
+ match = base_re.match(version_string)
+ if not match:
+ raise ValueError(
+ "Version string lacks a numerical component: %r"
+ % version_string
+ )
+
+ version = version_string[:match.end()]
+ if not partial:
+ # We need a not-partial version.
+ while version.count('.') < 2:
+ version += '.0'
+
+ # Strip leading zeros in components
+ # Version is of the form nn, nn.pp or nn.pp.qq
+ version = '.'.join(
+ # If the part was '0', we end up with an empty string.
+ part.lstrip('0') or '0'
+ for part in version.split('.')
+ )
+
+ if match.end() == len(version_string):
+ return Version(version, partial=partial)
+
+ rest = version_string[match.end():]
+
+ # Cleanup the 'rest'
+ rest = re.sub(r'[^a-zA-Z0-9+.-]', '-', rest)
+
+ if rest[0] == '+':
+ # A 'build' component
+ prerelease = ''
+ build = rest[1:]
+ elif rest[0] == '.':
+ # An extra version component, probably 'build'
+ prerelease = ''
+ build = rest[1:]
+ elif rest[0] == '-':
+ rest = rest[1:]
+ if '+' in rest:
+ prerelease, build = rest.split('+', 1)
+ else:
+ prerelease, build = rest, ''
+ elif '+' in rest:
+ prerelease, build = rest.split('+', 1)
+ else:
+ prerelease, build = rest, ''
+
+ build = build.replace('+', '.')
+
+ if prerelease:
+ version = '%s-%s' % (version, prerelease)
+ if build:
+ version = '%s+%s' % (version, build)
+
+ return cls(version, partial=partial)
+
+ @classmethod
+ def parse(cls, version_string, partial=False, coerce=False):
+ """Parse a version string into a Version() object.
+
+ Args:
+ version_string (str), the version string to parse
+ partial (bool), whether to accept incomplete input
+ coerce (bool), whether to try to map the passed in string into a
+ valid Version.
+ """
+ if not version_string:
+ raise ValueError('Invalid empty version string: %r' % version_string)
+
+ if partial:
+ version_re = cls.partial_version_re
+ else:
+ version_re = cls.version_re
+
+ match = version_re.match(version_string)
+ if not match:
+ raise ValueError('Invalid version string: %r' % version_string)
+
+ major, minor, patch, prerelease, build = match.groups()
+
+ if _has_leading_zero(major):
+ raise ValueError("Invalid leading zero in major: %r" % version_string)
+ if _has_leading_zero(minor):
+ raise ValueError("Invalid leading zero in minor: %r" % version_string)
+ if _has_leading_zero(patch):
+ raise ValueError("Invalid leading zero in patch: %r" % version_string)
+
+ major = int(major)
+ minor = cls._coerce(minor, partial)
+ patch = cls._coerce(patch, partial)
+
+ if prerelease is None:
+ if partial and (build is None):
+ # No build info, strip here
+ return (major, minor, patch, None, None)
+ else:
+ prerelease = ()
+ elif prerelease == '':
+ prerelease = ()
+ else:
+ prerelease = tuple(prerelease.split('.'))
+ cls._validate_identifiers(prerelease, allow_leading_zeroes=False)
+
+ if build is None:
+ if partial:
+ build = None
+ else:
+ build = ()
+ elif build == '':
+ build = ()
+ else:
+ build = tuple(build.split('.'))
+ cls._validate_identifiers(build, allow_leading_zeroes=True)
+
+ return (major, minor, patch, prerelease, build)
+
+ @classmethod
+ def _validate_identifiers(cls, identifiers, allow_leading_zeroes=False):
+ for item in identifiers:
+ if not item:
+ raise ValueError(
+ "Invalid empty identifier %r in %r"
+ % (item, '.'.join(identifiers))
+ )
+
+ if item[0] == '0' and item.isdigit() and item != '0' and not allow_leading_zeroes:
+ raise ValueError("Invalid leading zero in identifier %r" % item)
+
+ @classmethod
+ def _validate_kwargs(cls, major, minor, patch, prerelease, build, partial):
+ if (
+ major != int(major)
+ or minor != cls._coerce(minor, partial)
+ or patch != cls._coerce(patch, partial)
+ or prerelease is None and not partial
+ or build is None and not partial
+ ):
+ raise ValueError(
+ "Invalid kwargs to Version(major=%r, minor=%r, patch=%r, "
+ "prerelease=%r, build=%r, partial=%r" % (
+ major, minor, patch, prerelease, build, partial
+ ))
+ if prerelease is not None:
+ cls._validate_identifiers(prerelease, allow_leading_zeroes=False)
+ if build is not None:
+ cls._validate_identifiers(build, allow_leading_zeroes=True)
+
+ def __iter__(self):
+ return iter((self.major, self.minor, self.patch, self.prerelease, self.build))
+
+ def __str__(self):
+ version = '%d' % self.major
+ if self.minor is not None:
+ version = '%s.%d' % (version, self.minor)
+ if self.patch is not None:
+ version = '%s.%d' % (version, self.patch)
+
+ if self.prerelease or (self.partial and self.prerelease == () and self.build is None):
+ version = '%s-%s' % (version, '.'.join(self.prerelease))
+ if self.build or (self.partial and self.build == ()):
+ version = '%s+%s' % (version, '.'.join(self.build))
+ return version
+
+ def __repr__(self):
+ return '%s(%r%s)' % (
+ self.__class__.__name__,
+ str(self),
+ ', partial=True' if self.partial else '',
+ )
+
+ def __hash__(self):
+ # We don't include 'partial', since this is strictly equivalent to having
+ # at least a field being `None`.
+ return hash((self.major, self.minor, self.patch, self.prerelease, self.build))
+
+ @property
+ def precedence_key(self):
+ if self.prerelease:
+ prerelease_key = tuple(
+ NumericIdentifier(part) if re.match(r'^[0-9]+$', part) else AlphaIdentifier(part)
+ for part in self.prerelease
+ )
+ else:
+ prerelease_key = (
+ MaxIdentifier(),
+ )
+
+ return (
+ self.major,
+ self.minor,
+ self.patch,
+ prerelease_key,
+ )
+
+ def __cmp__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ if self < other:
+ return -1
+ elif self > other:
+ return 1
+ elif self == other:
+ return 0
+ else:
+ return NotImplemented
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return (
+ self.major == other.major
+ and self.minor == other.minor
+ and self.patch == other.patch
+ and (self.prerelease or ()) == (other.prerelease or ())
+ and (self.build or ()) == (other.build or ())
+ )
+
+ def __ne__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return tuple(self) != tuple(other)
+
+ def __lt__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.precedence_key < other.precedence_key
+
+ def __le__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.precedence_key <= other.precedence_key
+
+ def __gt__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.precedence_key > other.precedence_key
+
+ def __ge__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.precedence_key >= other.precedence_key
+
+
+class SpecItem(object):
+ """A requirement specification."""
+
+ KIND_ANY = '*'
+ KIND_LT = '<'
+ KIND_LTE = '<='
+ KIND_EQUAL = '=='
+ KIND_SHORTEQ = '='
+ KIND_EMPTY = ''
+ KIND_GTE = '>='
+ KIND_GT = '>'
+ KIND_NEQ = '!='
+ KIND_CARET = '^'
+ KIND_TILDE = '~'
+ KIND_COMPATIBLE = '~='
+
+ # Map a kind alias to its full version
+ KIND_ALIASES = {
+ KIND_SHORTEQ: KIND_EQUAL,
+ KIND_EMPTY: KIND_EQUAL,
+ }
+
+ re_spec = re.compile(r'^(<|<=||=|==|>=|>|!=|\^|~|~=)(\d.*)$')
+
+ def __init__(self, requirement_string, _warn=True):
+ if _warn:
+ warnings.warn(
+ "The `SpecItem` class will be removed in 3.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ kind, spec = self.parse(requirement_string)
+ self.kind = kind
+ self.spec = spec
+ self._clause = Spec(requirement_string).clause
+
+ @classmethod
+ def parse(cls, requirement_string):
+ if not requirement_string:
+ raise ValueError("Invalid empty requirement specification: %r" % requirement_string)
+
+ # Special case: the 'any' version spec.
+ if requirement_string == '*':
+ return (cls.KIND_ANY, '')
+
+ match = cls.re_spec.match(requirement_string)
+ if not match:
+ raise ValueError("Invalid requirement specification: %r" % requirement_string)
+
+ kind, version = match.groups()
+ if kind in cls.KIND_ALIASES:
+ kind = cls.KIND_ALIASES[kind]
+
+ spec = Version(version, partial=True)
+ if spec.build is not None and kind not in (cls.KIND_EQUAL, cls.KIND_NEQ):
+ raise ValueError(
+ "Invalid requirement specification %r: build numbers have no ordering."
+ % requirement_string
+ )
+ return (kind, spec)
+
+ @classmethod
+ def from_matcher(cls, matcher):
+ if matcher == Always():
+ return cls('*', _warn=False)
+ elif matcher == Never():
+ return cls('<0.0.0-', _warn=False)
+ elif isinstance(matcher, Range):
+ return cls('%s%s' % (matcher.operator, matcher.target), _warn=False)
+
+ def match(self, version):
+ return self._clause.match(version)
+
+ def __str__(self):
+ return '%s%s' % (self.kind, self.spec)
+
+ def __repr__(self):
+ return '<SpecItem: %s %r>' % (self.kind, self.spec)
+
+ def __eq__(self, other):
+ if not isinstance(other, SpecItem):
+ return NotImplemented
+ return self.kind == other.kind and self.spec == other.spec
+
+ def __hash__(self):
+ return hash((self.kind, self.spec))
+
+
+def compare(v1, v2):
+ return Version(v1).__cmp__(Version(v2))
+
+
+def match(spec, version):
+ return Spec(spec).match(Version(version))
+
+
+def validate(version_string):
+ """Validates a version string againt the SemVer specification."""
+ try:
+ Version.parse(version_string)
+ return True
+ except ValueError:
+ return False
+
+
+DEFAULT_SYNTAX = 'simple'
+
+
+class BaseSpec(object):
+ """A specification of compatible versions.
+
+ Usage:
+ >>> Spec('>=1.0.0', syntax='npm')
+
+ A version matches a specification if it matches any
+ of the clauses of that specification.
+
+ Internally, a Spec is AnyOf(
+ AllOf(Matcher, Matcher, Matcher),
+ AllOf(...),
+ )
+ """
+ SYNTAXES = {}
+
+ @classmethod
+ def register_syntax(cls, subclass):
+ syntax = subclass.SYNTAX
+ if syntax is None:
+ raise ValueError("A Spec needs its SYNTAX field to be set.")
+ elif syntax in cls.SYNTAXES:
+ raise ValueError(
+ "Duplicate syntax for %s: %r, %r"
+ % (syntax, cls.SYNTAXES[syntax], subclass)
+ )
+ cls.SYNTAXES[syntax] = subclass
+ return subclass
+
+ def __init__(self, expression):
+ super(BaseSpec, self).__init__()
+ self.expression = expression
+ self.clause = self._parse_to_clause(expression)
+
+ @classmethod
+ def parse(cls, expression, syntax=DEFAULT_SYNTAX):
+ """Convert a syntax-specific expression into a BaseSpec instance."""
+ return cls.SYNTAXES[syntax](expression)
+
+ @classmethod
+ def _parse_to_clause(cls, expression):
+ """Converts an expression to a clause."""
+ raise NotImplementedError()
+
+ def filter(self, versions):
+ """Filter an iterable of versions satisfying the Spec."""
+ for version in versions:
+ if self.match(version):
+ yield version
+
+ def match(self, version):
+ """Check whether a Version satisfies the Spec."""
+ return self.clause.match(version)
+
+ def select(self, versions):
+ """Select the best compatible version among an iterable of options."""
+ options = list(self.filter(versions))
+ if options:
+ return max(options)
+ return None
+
+ def __contains__(self, version):
+ """Whether `version in self`."""
+ if isinstance(version, Version):
+ return self.match(version)
+ return False
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+
+ return self.clause == other.clause
+
+ def __hash__(self):
+ return hash(self.clause)
+
+ def __str__(self):
+ return self.expression
+
+ def __repr__(self):
+ return '<%s: %r>' % (self.__class__.__name__, self.expression)
+
+
+class Clause(object):
+ __slots__ = []
+
+ def match(self, version):
+ raise NotImplementedError()
+
+ def __and__(self, other):
+ raise NotImplementedError()
+
+ def __or__(self, other):
+ raise NotImplementedError()
+
+ def __eq__(self, other):
+ raise NotImplementedError()
+
+ def prettyprint(self, indent='\t'):
+ """Pretty-print the clause.
+ """
+ return '\n'.join(self._pretty()).replace('\t', indent)
+
+ def _pretty(self):
+ """Actual pretty-printing logic.
+
+ Yields:
+ A list of string. Indentation is performed with \t.
+ """
+ yield repr(self)
+
+ def __ne__(self, other):
+ return not self == other
+
+ def simplify(self):
+ return self
+
+
+class AnyOf(Clause):
+ __slots__ = ['clauses']
+
+ def __init__(self, *clauses):
+ super(AnyOf, self).__init__()
+ self.clauses = frozenset(clauses)
+
+ def match(self, version):
+ return any(c.match(version) for c in self.clauses)
+
+ def simplify(self):
+ subclauses = set()
+ for clause in self.clauses:
+ simplified = clause.simplify()
+ if isinstance(simplified, AnyOf):
+ subclauses |= simplified.clauses
+ elif simplified == Never():
+ continue
+ else:
+ subclauses.add(simplified)
+ if len(subclauses) == 1:
+ return subclauses.pop()
+ return AnyOf(*subclauses)
+
+ def __hash__(self):
+ return hash((AnyOf, self.clauses))
+
+ def __iter__(self):
+ return iter(self.clauses)
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.clauses == other.clauses
+
+ def __and__(self, other):
+ if isinstance(other, AllOf):
+ return other & self
+ elif isinstance(other, Matcher) or isinstance(other, AnyOf):
+ return AllOf(self, other)
+ else:
+ return NotImplemented
+
+ def __or__(self, other):
+ if isinstance(other, AnyOf):
+ clauses = list(self.clauses | other.clauses)
+ elif isinstance(other, Matcher) or isinstance(other, AllOf):
+ clauses = list(self.clauses | set([other]))
+ else:
+ return NotImplemented
+ return AnyOf(*clauses)
+
+ def __repr__(self):
+ return 'AnyOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses))
+
+ def _pretty(self):
+ yield 'AnyOF('
+ for clause in self.clauses:
+ lines = list(clause._pretty())
+ for line in lines[:-1]:
+ yield '\t' + line
+ yield '\t' + lines[-1] + ','
+ yield ')'
+
+
+class AllOf(Clause):
+ __slots__ = ['clauses']
+
+ def __init__(self, *clauses):
+ super(AllOf, self).__init__()
+ self.clauses = frozenset(clauses)
+
+ def match(self, version):
+ return all(clause.match(version) for clause in self.clauses)
+
+ def simplify(self):
+ subclauses = set()
+ for clause in self.clauses:
+ simplified = clause.simplify()
+ if isinstance(simplified, AllOf):
+ subclauses |= simplified.clauses
+ elif simplified == Always():
+ continue
+ else:
+ subclauses.add(simplified)
+ if len(subclauses) == 1:
+ return subclauses.pop()
+ return AllOf(*subclauses)
+
+ def __hash__(self):
+ return hash((AllOf, self.clauses))
+
+ def __iter__(self):
+ return iter(self.clauses)
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.clauses == other.clauses
+
+ def __and__(self, other):
+ if isinstance(other, Matcher) or isinstance(other, AnyOf):
+ clauses = list(self.clauses | set([other]))
+ elif isinstance(other, AllOf):
+ clauses = list(self.clauses | other.clauses)
+ else:
+ return NotImplemented
+ return AllOf(*clauses)
+
+ def __or__(self, other):
+ if isinstance(other, AnyOf):
+ return other | self
+ elif isinstance(other, Matcher):
+ return AnyOf(self, AllOf(other))
+ elif isinstance(other, AllOf):
+ return AnyOf(self, other)
+ else:
+ return NotImplemented
+
+ def __repr__(self):
+ return 'AllOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses))
+
+ def _pretty(self):
+ yield 'AllOF('
+ for clause in self.clauses:
+ lines = list(clause._pretty())
+ for line in lines[:-1]:
+ yield '\t' + line
+ yield '\t' + lines[-1] + ','
+ yield ')'
+
+
+class Matcher(Clause):
+ __slots__ = []
+
+ def __and__(self, other):
+ if isinstance(other, AllOf):
+ return other & self
+ elif isinstance(other, Matcher) or isinstance(other, AnyOf):
+ return AllOf(self, other)
+ else:
+ return NotImplemented
+
+ def __or__(self, other):
+ if isinstance(other, AnyOf):
+ return other | self
+ elif isinstance(other, Matcher) or isinstance(other, AllOf):
+ return AnyOf(self, other)
+ else:
+ return NotImplemented
+
+
+class Never(Matcher):
+ __slots__ = []
+
+ def match(self, version):
+ return False
+
+ def __hash__(self):
+ return hash((Never,))
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ def __and__(self, other):
+ return self
+
+ def __or__(self, other):
+ return other
+
+ def __repr__(self):
+ return 'Never()'
+
+
+class Always(Matcher):
+ __slots__ = []
+
+ def match(self, version):
+ return True
+
+ def __hash__(self):
+ return hash((Always,))
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ def __and__(self, other):
+ return other
+
+ def __or__(self, other):
+ return self
+
+ def __repr__(self):
+ return 'Always()'
+
+
+class Range(Matcher):
+ OP_EQ = '=='
+ OP_GT = '>'
+ OP_GTE = '>='
+ OP_LT = '<'
+ OP_LTE = '<='
+ OP_NEQ = '!='
+
+ # <1.2.3 matches 1.2.3-a1
+ PRERELEASE_ALWAYS = 'always'
+ # <1.2.3 does not match 1.2.3-a1
+ PRERELEASE_NATURAL = 'natural'
+ # 1.2.3-a1 is only considered if target == 1.2.3-xxx
+ PRERELEASE_SAMEPATCH = 'same-patch'
+
+ # 1.2.3 matches 1.2.3+*
+ BUILD_IMPLICIT = 'implicit'
+ # 1.2.3 matches only 1.2.3, not 1.2.3+4
+ BUILD_STRICT = 'strict'
+
+ __slots__ = ['operator', 'target', 'prerelease_policy', 'build_policy']
+
+ def __init__(self, operator, target, prerelease_policy=PRERELEASE_NATURAL, build_policy=BUILD_IMPLICIT):
+ super(Range, self).__init__()
+ if target.build and operator not in (self.OP_EQ, self.OP_NEQ):
+ raise ValueError(
+ "Invalid range %s%s: build numbers have no ordering."
+ % (operator, target))
+ self.operator = operator
+ self.target = target
+ self.prerelease_policy = prerelease_policy
+ self.build_policy = self.BUILD_STRICT if target.build else build_policy
+
+ def match(self, version):
+ if self.build_policy != self.BUILD_STRICT:
+ version = version.truncate('prerelease')
+
+ if version.prerelease:
+ same_patch = self.target.truncate() == version.truncate()
+
+ if self.prerelease_policy == self.PRERELEASE_SAMEPATCH and not same_patch:
+ return False
+
+ if self.operator == self.OP_EQ:
+ if self.build_policy == self.BUILD_STRICT:
+ return (
+ self.target.truncate('prerelease') == version.truncate('prerelease')
+ and version.build == self.target.build
+ )
+ return version == self.target
+ elif self.operator == self.OP_GT:
+ return version > self.target
+ elif self.operator == self.OP_GTE:
+ return version >= self.target
+ elif self.operator == self.OP_LT:
+ if (
+ version.prerelease
+ and self.prerelease_policy == self.PRERELEASE_NATURAL
+ and version.truncate() == self.target.truncate()
+ and not self.target.prerelease
+ ):
+ return False
+ return version < self.target
+ elif self.operator == self.OP_LTE:
+ return version <= self.target
+ else:
+ assert self.operator == self.OP_NEQ
+ if self.build_policy == self.BUILD_STRICT:
+ return not (
+ self.target.truncate('prerelease') == version.truncate('prerelease')
+ and version.build == self.target.build
+ )
+
+ if (
+ version.prerelease
+ and self.prerelease_policy == self.PRERELEASE_NATURAL
+ and version.truncate() == self.target.truncate()
+ and not self.target.prerelease
+ ):
+ return False
+ return version != self.target
+
+ def __hash__(self):
+ return hash((Range, self.operator, self.target, self.prerelease_policy))
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, self.__class__)
+ and self.operator == other.operator
+ and self.target == other.target
+ and self.prerelease_policy == other.prerelease_policy
+ )
+
+ def __str__(self):
+ return '%s%s' % (self.operator, self.target)
+
+ def __repr__(self):
+ policy_part = (
+ '' if self.prerelease_policy == self.PRERELEASE_NATURAL
+ else ', prerelease_policy=%r' % self.prerelease_policy
+ ) + (
+ '' if self.build_policy == self.BUILD_IMPLICIT
+ else ', build_policy=%r' % self.build_policy
+ )
+ return 'Range(%r, %r%s)' % (
+ self.operator,
+ self.target,
+ policy_part,
+ )
+
+
+@BaseSpec.register_syntax
+class SimpleSpec(BaseSpec):
+
+ SYNTAX = 'simple'
+
+ @classmethod
+ def _parse_to_clause(cls, expression):
+ return cls.Parser.parse(expression)
+
+ class Parser:
+ NUMBER = r'\*|0|[1-9][0-9]*'
+ NAIVE_SPEC = re.compile(r"""^
+ (?P<op><|<=||=|==|>=|>|!=|\^|~|~=)
+ (?P<major>{nb})(?:\.(?P<minor>{nb})(?:\.(?P<patch>{nb}))?)?
+ (?:-(?P<prerel>[a-z0-9A-Z.-]*))?
+ (?:\+(?P<build>[a-z0-9A-Z.-]*))?
+ $
+ """.format(nb=NUMBER),
+ re.VERBOSE,
+ )
+
+ @classmethod
+ def parse(cls, expression):
+ blocks = expression.split(',')
+ clause = Always()
+ for block in blocks:
+ if not cls.NAIVE_SPEC.match(block):
+ raise ValueError("Invalid simple block %r" % block)
+ clause &= cls.parse_block(block)
+
+ return clause
+
+ PREFIX_CARET = '^'
+ PREFIX_TILDE = '~'
+ PREFIX_COMPATIBLE = '~='
+ PREFIX_EQ = '=='
+ PREFIX_NEQ = '!='
+ PREFIX_GT = '>'
+ PREFIX_GTE = '>='
+ PREFIX_LT = '<'
+ PREFIX_LTE = '<='
+
+ PREFIX_ALIASES = {
+ '=': PREFIX_EQ,
+ '': PREFIX_EQ,
+ }
+
+ EMPTY_VALUES = ['*', 'x', 'X', None]
+
+ @classmethod
+ def parse_block(cls, expr):
+ if not cls.NAIVE_SPEC.match(expr):
+ raise ValueError("Invalid simple spec component: %r" % expr)
+ prefix, major_t, minor_t, patch_t, prerel, build = cls.NAIVE_SPEC.match(expr).groups()
+ prefix = cls.PREFIX_ALIASES.get(prefix, prefix)
+
+ major = None if major_t in cls.EMPTY_VALUES else int(major_t)
+ minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t)
+ patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t)
+
+ if major is None: # '*'
+ target = Version(major=0, minor=0, patch=0)
+ if prefix not in (cls.PREFIX_EQ, cls.PREFIX_GTE):
+ raise ValueError("Invalid simple spec: %r" % expr)
+ elif minor is None:
+ target = Version(major=major, minor=0, patch=0)
+ elif patch is None:
+ target = Version(major=major, minor=minor, patch=0)
+ else:
+ target = Version(
+ major=major,
+ minor=minor,
+ patch=patch,
+ prerelease=prerel.split('.') if prerel else (),
+ build=build.split('.') if build else (),
+ )
+
+ if (major is None or minor is None or patch is None) and (prerel or build):
+ raise ValueError("Invalid simple spec: %r" % expr)
+
+ if build is not None and prefix not in (cls.PREFIX_EQ, cls.PREFIX_NEQ):
+ raise ValueError("Invalid simple spec: %r" % expr)
+
+ if prefix == cls.PREFIX_CARET:
+ # Accept anything with the same most-significant digit
+ if target.major:
+ high = target.next_major()
+ elif target.minor:
+ high = target.next_minor()
+ else:
+ high = target.next_patch()
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+ elif prefix == cls.PREFIX_TILDE:
+ assert major is not None
+ # Accept any higher patch in the same minor
+ # Might go higher if the initial version was a partial
+ if minor is None:
+ high = target.next_major()
+ else:
+ high = target.next_minor()
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+ elif prefix == cls.PREFIX_COMPATIBLE:
+ assert major is not None
+ # ~1 is 1.0.0..2.0.0; ~=2.2 is 2.2.0..3.0.0; ~=1.4.5 is 1.4.5..1.5.0
+ if minor is None or patch is None:
+ # We got a partial version
+ high = target.next_major()
+ else:
+ high = target.next_minor()
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+ elif prefix == cls.PREFIX_EQ:
+ if major is None:
+ return Range(Range.OP_GTE, target)
+ elif minor is None:
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, target.next_major())
+ elif patch is None:
+ return Range(Range.OP_GTE, target) & Range(Range.OP_LT, target.next_patch())
+ elif build == '':
+ return Range(Range.OP_EQ, target, build_policy=Range.BUILD_STRICT)
+ else:
+ return Range(Range.OP_EQ, target)
+
+ elif prefix == cls.PREFIX_NEQ:
+ assert major is not None
+ if minor is None:
+ # !=1.x => <1.0.0 || >=2.0.0
+ return Range(Range.OP_LT, target) | Range(Range.OP_GTE, target.next_major())
+ elif patch is None:
+ # !=1.2.x => <1.2.0 || >=1.3.0
+ return Range(Range.OP_LT, target) | Range(Range.OP_GTE, target.next_minor())
+ elif prerel == '':
+ # !=1.2.3-
+ return Range(Range.OP_NEQ, target, prerelease_policy=Range.PRERELEASE_ALWAYS)
+ elif build == '':
+ # !=1.2.3+ or !=1.2.3-a2+
+ return Range(Range.OP_NEQ, target, build_policy=Range.BUILD_STRICT)
+ else:
+ return Range(Range.OP_NEQ, target)
+
+ elif prefix == cls.PREFIX_GT:
+ assert major is not None
+ if minor is None:
+ # >1.x => >=2.0
+ return Range(Range.OP_GTE, target.next_major())
+ elif patch is None:
+ return Range(Range.OP_GTE, target.next_minor())
+ else:
+ return Range(Range.OP_GT, target)
+
+ elif prefix == cls.PREFIX_GTE:
+ return Range(Range.OP_GTE, target)
+
+ elif prefix == cls.PREFIX_LT:
+ assert major is not None
+ if prerel == '':
+ # <1.2.3-
+ return Range(Range.OP_LT, target, prerelease_policy=Range.PRERELEASE_ALWAYS)
+ return Range(Range.OP_LT, target)
+
+ else:
+ assert prefix == cls.PREFIX_LTE
+ assert major is not None
+ if minor is None:
+ # <=1.x => <2.0
+ return Range(Range.OP_LT, target.next_major())
+ elif patch is None:
+ return Range(Range.OP_LT, target.next_minor())
+ else:
+ return Range(Range.OP_LTE, target)
+
+
+class LegacySpec(SimpleSpec):
+ def __init__(self, *expressions):
+ warnings.warn(
+ "The Spec() class will be removed in 3.1; use SimpleSpec() instead.",
+ PendingDeprecationWarning,
+ stacklevel=2,
+ )
+
+ if len(expressions) > 1:
+ warnings.warn(
+ "Passing 2+ arguments to SimpleSpec will be removed in 3.0; concatenate them with ',' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ expression = ','.join(expressions)
+ super(LegacySpec, self).__init__(expression)
+
+ @property
+ def specs(self):
+ return list(self)
+
+ def __iter__(self):
+ warnings.warn(
+ "Iterating over the components of a SimpleSpec object will be removed in 3.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ try:
+ clauses = list(self.clause)
+ except TypeError: # Not an iterable
+ clauses = [self.clause]
+ for clause in clauses:
+ yield SpecItem.from_matcher(clause)
+
+
+Spec = LegacySpec
+
+
+@BaseSpec.register_syntax
+class NpmSpec(BaseSpec):
+ SYNTAX = 'npm'
+
+ @classmethod
+ def _parse_to_clause(cls, expression):
+ return cls.Parser.parse(expression)
+
+ class Parser:
+ JOINER = '||'
+ HYPHEN = ' - '
+
+ NUMBER = r'x|X|\*|0|[1-9][0-9]*'
+ PART = r'[a-zA-Z0-9.-]*'
+ NPM_SPEC_BLOCK = re.compile(r"""
+ ^(?:v)? # Strip optional initial v
+ (?P<op><|<=|>=|>|=|\^|~|) # Operator, can be empty
+ (?P<major>{nb})(?:\.(?P<minor>{nb})(?:\.(?P<patch>{nb}))?)?
+ (?:-(?P<prerel>{part}))? # Optional re-release
+ (?:\+(?P<build>{part}))? # Optional build
+ $""".format(nb=NUMBER, part=PART),
+ re.VERBOSE,
+ )
+
+ @classmethod
+ def range(cls, operator, target):
+ return Range(operator, target, prerelease_policy=Range.PRERELEASE_SAMEPATCH)
+
+ @classmethod
+ def parse(cls, expression):
+ result = Never()
+ groups = expression.split(cls.JOINER)
+ for group in groups:
+ group = group.strip()
+ if not group:
+ group = '>=0.0.0'
+
+ subclauses = []
+ if cls.HYPHEN in group:
+ low, high = group.split(cls.HYPHEN, 2)
+ subclauses = cls.parse_simple('>=' + low) + cls.parse_simple('<=' + high)
+
+ else:
+ blocks = group.split(' ')
+ for block in blocks:
+ if not cls.NPM_SPEC_BLOCK.match(block):
+ raise ValueError("Invalid NPM block in %r: %r" % (expression, block))
+
+ subclauses.extend(cls.parse_simple(block))
+
+ prerelease_clauses = []
+ non_prerel_clauses = []
+ for clause in subclauses:
+ if clause.target.prerelease:
+ if clause.operator in (Range.OP_GT, Range.OP_GTE):
+ prerelease_clauses.append(Range(
+ operator=Range.OP_LT,
+ target=Version(
+ major=clause.target.major,
+ minor=clause.target.minor,
+ patch=clause.target.patch + 1,
+ ),
+ prerelease_policy=Range.PRERELEASE_ALWAYS,
+ ))
+ elif clause.operator in (Range.OP_LT, Range.OP_LTE):
+ prerelease_clauses.append(Range(
+ operator=Range.OP_GTE,
+ target=Version(
+ major=clause.target.major,
+ minor=clause.target.minor,
+ patch=0,
+ prerelease=(),
+ ),
+ prerelease_policy=Range.PRERELEASE_ALWAYS,
+ ))
+ prerelease_clauses.append(clause)
+ non_prerel_clauses.append(cls.range(
+ operator=clause.operator,
+ target=clause.target.truncate(),
+ ))
+ else:
+ non_prerel_clauses.append(clause)
+ if prerelease_clauses:
+ result |= AllOf(*prerelease_clauses)
+ result |= AllOf(*non_prerel_clauses)
+
+ return result
+
+ PREFIX_CARET = '^'
+ PREFIX_TILDE = '~'
+ PREFIX_EQ = '='
+ PREFIX_GT = '>'
+ PREFIX_GTE = '>='
+ PREFIX_LT = '<'
+ PREFIX_LTE = '<='
+
+ PREFIX_ALIASES = {
+ '': PREFIX_EQ,
+ }
+
+ PREFIX_TO_OPERATOR = {
+ PREFIX_EQ: Range.OP_EQ,
+ PREFIX_LT: Range.OP_LT,
+ PREFIX_LTE: Range.OP_LTE,
+ PREFIX_GTE: Range.OP_GTE,
+ PREFIX_GT: Range.OP_GT,
+ }
+
+ EMPTY_VALUES = ['*', 'x', 'X', None]
+
+ @classmethod
+ def parse_simple(cls, simple):
+ match = cls.NPM_SPEC_BLOCK.match(simple)
+
+ prefix, major_t, minor_t, patch_t, prerel, build = match.groups()
+
+ prefix = cls.PREFIX_ALIASES.get(prefix, prefix)
+ major = None if major_t in cls.EMPTY_VALUES else int(major_t)
+ minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t)
+ patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t)
+
+ if build is not None and prefix not in [cls.PREFIX_EQ]:
+ # Ignore the 'build' part when not comparing to a specific part.
+ build = None
+
+ if major is None: # '*', 'x', 'X'
+ target = Version(major=0, minor=0, patch=0)
+ if prefix not in [cls.PREFIX_EQ, cls.PREFIX_GTE]:
+ raise ValueError("Invalid expression %r" % simple)
+ prefix = cls.PREFIX_GTE
+ elif minor is None:
+ target = Version(major=major, minor=0, patch=0)
+ elif patch is None:
+ target = Version(major=major, minor=minor, patch=0)
+ else:
+ target = Version(
+ major=major,
+ minor=minor,
+ patch=patch,
+ prerelease=prerel.split('.') if prerel else (),
+ build=build.split('.') if build else (),
+ )
+
+ if (major is None or minor is None or patch is None) and (prerel or build):
+ raise ValueError("Invalid NPM spec: %r" % simple)
+
+ if prefix == cls.PREFIX_CARET:
+ if target.major: # ^1.2.4 => >=1.2.4 <2.0.0 ; ^1.x => >=1.0.0 <2.0.0
+ high = target.truncate().next_major()
+ elif target.minor: # ^0.1.2 => >=0.1.2 <0.2.0
+ high = target.truncate().next_minor()
+ elif minor is None: # ^0.x => >=0.0.0 <1.0.0
+ high = target.truncate().next_major()
+ elif patch is None: # ^0.2.x => >=0.2.0 <0.3.0
+ high = target.truncate().next_minor()
+ else: # ^0.0.1 => >=0.0.1 <0.0.2
+ high = target.truncate().next_patch()
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, high)]
+
+ elif prefix == cls.PREFIX_TILDE:
+ assert major is not None
+ if minor is None: # ~1.x => >=1.0.0 <2.0.0
+ high = target.next_major()
+ else: # ~1.2.x => >=1.2.0 <1.3.0; ~1.2.3 => >=1.2.3 <1.3.0
+ high = target.next_minor()
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, high)]
+
+ elif prefix == cls.PREFIX_EQ:
+ if major is None:
+ return [cls.range(Range.OP_GTE, target)]
+ elif minor is None:
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, target.next_major())]
+ elif patch is None:
+ return [cls.range(Range.OP_GTE, target), cls.range(Range.OP_LT, target.next_minor())]
+ else:
+ return [cls.range(Range.OP_EQ, target)]
+
+ elif prefix == cls.PREFIX_GT:
+ assert major is not None
+ if minor is None: # >1.x
+ return [cls.range(Range.OP_GTE, target.next_major())]
+ elif patch is None: # >1.2.x => >=1.3.0
+ return [cls.range(Range.OP_GTE, target.next_minor())]
+ else:
+ return [cls.range(Range.OP_GT, target)]
+
+ elif prefix == cls.PREFIX_GTE:
+ return [cls.range(Range.OP_GTE, target)]
+
+ elif prefix == cls.PREFIX_LT:
+ assert major is not None
+ return [cls.range(Range.OP_LT, target)]
+
+ else:
+ assert prefix == cls.PREFIX_LTE
+ assert major is not None
+ if minor is None: # <=1.x => <2.0.0
+ return [cls.range(Range.OP_LT, target.next_major())]
+ elif patch is None: # <=1.2.x => <1.3.0
+ return [cls.range(Range.OP_LT, target.next_minor())]
+ else:
+ return [cls.range(Range.OP_LTE, target)]
diff --git a/talerbuildconfig.py b/talerbuildconfig.py
index e243a19..f28e1c2 100644
--- a/talerbuildconfig.py
+++ b/talerbuildconfig.py
@@ -35,6 +35,7 @@ import logging
from distutils.spawn import find_executable
import subprocess
from dataclasses import dataclass
+import semver
"""
This module aims to replicate a small GNU Coding Standards
@@ -50,6 +51,9 @@ Makefile fragement, which is the processed by a Makefile (usually) in
GNU Make format.
"""
+# Should be incremented each time we add some functionality
+serialversion = 2
+
# TODO: We need a smallest version argument.
@@ -116,9 +120,17 @@ class BuildConfig:
for tool in self.tools:
res = tool.check(self)
if not res:
- print(f"Error: tool {tool.name} not available")
+ print(f"Error: tool '{tool.name}' not available")
if hasattr(tool, "hint"):
print(f"Hint: {tool.hint}")
+ sys.exit(1)
+ if hasattr(tool, "version_spec"):
+ sv = semver.SimpleSpec(tool.version_spec)
+ path, version = self.tool_results[tool.name]
+ if not sv.match(semver.Version(version)):
+ print(f"Error: Tool '{tool.name}' has version '{version}', but we require '{tool.version_spec}'")
+ sys.exit(1)
+
for tool in self.tools:
path, version = self.tool_results[tool.name]
@@ -413,6 +425,9 @@ class NodeJsTool(Tool):
name = "node"
hint = "If you are using Ubuntu Linux or Debian Linux, try installing the\nnode-legacy package or symlink node to nodejs."
+ def __init__(self, version_spec):
+ self.version_spec = version_spec
+
def args(self, parser):
pass
@@ -427,7 +442,7 @@ class NodeJsTool(Tool):
):
buildconfig._warn("your node version is too old, use Node 4.x or newer")
return False
- node_version = tool_version("node --version")
+ node_version = tool_version("node --version").lstrip("v")
buildconfig._set_tool("node", "node", version=node_version)
return True
diff --git a/testconfigure.py b/testconfigure.py
index 29d82a1..3db0aa4 100644
--- a/testconfigure.py
+++ b/testconfigure.py
@@ -6,7 +6,7 @@ b.enable_configmk()
b.add_tool(YarnTool())
b.add_tool(BrowserTool())
b.add_tool(PyBabelTool())
-b.add_tool(NodeJsTool())
+b.add_tool(NodeJsTool(version_spec=">=12.0.0"))
b.add_tool(PythonTool())
b.add_tool(PosixTool("find"))
b.add_tool(PosixTool("xargs"))