gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-build-common] branch master updated (d81bbfa -> b260431)


From: gnunet
Subject: [taler-build-common] branch master updated (d81bbfa -> b260431)
Date: Mon, 30 Mar 2020 09:18:14 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a change to branch master
in repository build-common.

    from d81bbfa  force getting remotes
     new c4a6a98  check python version
     new b260431  add version range checks

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 semver.py           | 1420 +++++++++++++++++++++++++++++++++++++++++++++++++++
 talerbuildconfig.py |   26 +-
 testconfigure.py    |    2 +-
 3 files changed, 1445 insertions(+), 3 deletions(-)
 create mode 100644 semver.py

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 aa03862..f28e1c2 100644
--- a/talerbuildconfig.py
+++ b/talerbuildconfig.py
@@ -19,6 +19,13 @@
 #
 # SPDX-License-Identifier: 0BSD
 
+import sys
+
+if not (sys.version_info.major == 3 and sys.version_info.minor >= 7):
+    print("This script requires Python 3.7 or higher!")
+    print("You are using Python {}.{}.".format(sys.version_info.major, 
sys.version_info.minor))
+    sys.exit(1)
+
 from abc import ABC
 import argparse
 import os
@@ -28,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
@@ -43,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.
 
@@ -109,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]
@@ -406,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
 
@@ -420,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"))

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]