Blame SOURCES/nodejs.req

3cc9b1
#!/usr/bin/python
57e3e8
# -*- coding: utf-8 -*-
3cc9b1
# Copyright 2012, 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
57e3e8
# Copyright 2019 Jan Staněk <jstanek@redat.com>
3cc9b1
#
3cc9b1
# Permission is hereby granted, free of charge, to any person obtaining a copy
3cc9b1
# of this software and associated documentation files (the "Software"), to
3cc9b1
# deal in the Software without restriction, including without limitation the
3cc9b1
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
3cc9b1
# sell copies of the Software, and to permit persons to whom the Software is
3cc9b1
# furnished to do so, subject to the following conditions:
3cc9b1
#
3cc9b1
# The above copyright notice and this permission notice shall be included in
3cc9b1
# all copies or substantial portions of the Software.
57e3e8
#
3cc9b1
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3cc9b1
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3cc9b1
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3cc9b1
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3cc9b1
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
3cc9b1
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
3cc9b1
# IN THE SOFTWARE.
3cc9b1
57e3e8
""" Automatic dependency generator for Node.js libraries.
57e3e8
57e3e8
Metadata parsed from package.json.  See `man npm-json` for details.
57e3e8
"""
57e3e8
57e3e8
from __future__ import print_function
57e3e8
from __future__ import with_statement
57e3e8
3cc9b1
import json
57e3e8
import operator
3cc9b1
import os
3cc9b1
import re
3cc9b1
import sys
57e3e8
from collections import namedtuple
57e3e8
from itertools import chain
57e3e8
from itertools import takewhile
57e3e8
57e3e8
# Python version detection
57e3e8
_PY2 = sys.version_info[0] <= 2
57e3e8
_PY3 = sys.version_info[0] >= 3
57e3e8
57e3e8
if _PY2:
57e3e8
    from future_builtins import map, filter
57e3e8
57e3e8
57e3e8
#: Name format of the requirements
57e3e8
REQUIREMENT_NAME_TEMPLATE = "rh-nodejs8-npm({name})"
57e3e8
57e3e8
#: ``simple`` product of the NPM semver grammar.
57e3e8
RANGE_SPECIFIER_SIMPLE = re.compile(
57e3e8
    r"""
57e3e8
    (?P<operator>
57e3e8
        <= | >= | < | > | =     # primitive
57e3e8
        | ~ | \^                # tilde/caret operators
57e3e8
    )?
57e3e8
    \s*(?P<version>\S+)\s*  # version specifier
57e3e8
    """,
57e3e8
    flags=re.VERBOSE,
57e3e8
)
57e3e8
57e3e8
57e3e8
class UnsupportedVersionToken(ValueError):
57e3e8
    """Version specifier contains token unsupported by the parser."""
57e3e8
57e3e8
57e3e8
class Version(tuple):
57e3e8
    """Normalized RPM/NPM version.
57e3e8
57e3e8
    The version has up to 3 components – major, minor, patch.
57e3e8
    Any part set to None is treated as unspecified.
57e3e8
57e3e8
    ::
57e3e8
57e3e8
        1.2.3 == Version(1, 2, 3)
57e3e8
        1.2   == Version(1, 2)
57e3e8
        1     == Version(1)
57e3e8
        *     == Version()
57e3e8
    """
57e3e8
57e3e8
    __slots__ = ()
57e3e8
57e3e8
    #: Version part meaning 'Any'
57e3e8
    #: ``xr`` in https://docs.npmjs.com/misc/semver#range-grammar
57e3e8
    _PART_ANY = re.compile(r"^[xX*]$")
57e3e8
    #: Numeric version part
57e3e8
    #: ``nr`` in https://docs.npmjs.com/misc/semver#range-grammar
57e3e8
    _PART_NUMERIC = re.compile(r"0|[1-9]\d*")
57e3e8
57e3e8
    def __new__(cls, *args):
57e3e8
        """Create new version.
57e3e8
57e3e8
        Arguments:
57e3e8
            Version components in the order of "major", "minor", "patch".
57e3e8
            All parts are optional::
57e3e8
57e3e8
                >>> Version(1, 2, 3)
57e3e8
                Version(1, 2, 3)
57e3e8
                >>> Version(1)
57e3e8
                Version(1)
57e3e8
                >>> Version()
57e3e8
                Version()
57e3e8
57e3e8
        Returns:
57e3e8
            New Version.
57e3e8
        """
57e3e8
57e3e8
        if len(args) > 3:
57e3e8
            raise ValueError("Version has maximum of 3 components")
57e3e8
        return super(Version, cls).__new__(cls, map(int, args))
57e3e8
57e3e8
    def __repr__(self):
57e3e8
        """Pretty debugging format."""
57e3e8
57e3e8
        return "{0}({1})".format(self.__class__.__name__, ", ".join(map(str, self)))
57e3e8
57e3e8
    def __str__(self):
57e3e8
        """RPM version format."""
57e3e8
57e3e8
        return ".".join(format(part, "d") for part in self)
57e3e8
57e3e8
    @property
57e3e8
    def major(self):
57e3e8
        """Major version number, if any."""
57e3e8
        return self[0] if len(self) > 0 else None
57e3e8
57e3e8
    @property
57e3e8
    def minor(self):
57e3e8
        """Major version number, if any."""
57e3e8
        return self[1] if len(self) > 1 else None
57e3e8
57e3e8
    @property
57e3e8
    def patch(self):
57e3e8
        """Major version number, if any."""
57e3e8
        return self[2] if len(self) > 2 else None
57e3e8
57e3e8
    @property
57e3e8
    def empty(self):
57e3e8
        """True if the version contains nothing but zeroes."""
57e3e8
        return not any(self)
57e3e8
57e3e8
    @classmethod
57e3e8
    def parse(cls, version_string):
57e3e8
        """Parse individual version string (like ``1.2.3``) into Version.
57e3e8
57e3e8
        This is the ``partial`` production in the grammar:
57e3e8
        https://docs.npmjs.com/misc/semver#range-grammar
57e3e8
57e3e8
        Examples::
57e3e8
57e3e8
            >>> Version.parse("1.2.3")
57e3e8
            Version(1, 2, 3)
57e3e8
            >>> Version.parse("v2.x")
57e3e8
            Version(2)
57e3e8
            >>> Version.parse("")
57e3e8
            Version()
57e3e8
57e3e8
        Arguments:
57e3e8
            version_string (str): The version_string to parse.
57e3e8
57e3e8
        Returns:
57e3e8
            Version: Parsed result.
57e3e8
        """
57e3e8
57e3e8
        # Ignore leading ``v``, if any
57e3e8
        version_string = version_string.lstrip("v")
57e3e8
57e3e8
        part_list = version_string.split(".", 2)
57e3e8
        # Use only parts up to first "Any" indicator
57e3e8
        part_list = list(takewhile(lambda p: not cls._PART_ANY.match(p), part_list))
57e3e8
57e3e8
        if not part_list:
57e3e8
            return cls()
57e3e8
57e3e8
        # Strip off and discard any pre-release or build qualifiers at the end.
57e3e8
        # We can get away with this, because there is no sane way to represent
57e3e8
        # these kinds of version requirements in RPM, and we generally expect
57e3e8
        # the distro will only carry proper releases anyway.
57e3e8
        try:
57e3e8
            part_list[-1] = cls._PART_NUMERIC.match(part_list[-1]).group()
57e3e8
        except AttributeError:  # no match
57e3e8
            part_list.pop()
57e3e8
57e3e8
        # Extend with ``None``s at the end, if necessary
57e3e8
        return cls(*part_list)
57e3e8
57e3e8
    def incremented(self):
57e3e8
        """Increment the least significant part of the version::
57e3e8
57e3e8
            >>> Version(1, 2, 3).incremented()
57e3e8
            Version(1, 2, 4)
57e3e8
            >>> Version(1, 2).incremented()
57e3e8
            Version(1, 3)
57e3e8
            >>> Version(1).incremented()
57e3e8
            Version(2)
57e3e8
            >>> Version().incremented()
57e3e8
            Version()
57e3e8
57e3e8
        Returns:
57e3e8
            Version: New incremented Version.
57e3e8
        """
57e3e8
57e3e8
        if len(self) == 0:
57e3e8
            return self.__class__()
57e3e8
        else:
57e3e8
            args = self[:-1] + (self[-1] + 1,)
57e3e8
            return self.__class__(*args)
57e3e8
57e3e8
57e3e8
class VersionBoundary(namedtuple("VersionBoundary", ("version", "operator"))):
57e3e8
    """Normalized version range boundary."""
57e3e8
57e3e8
    __slots__ = ()
57e3e8
57e3e8
    #: Ordering of primitive operators.
57e3e8
    #: Operators not listed here are handled specially; see __compare below.
57e3e8
    #: Convention: Lower boundary < 0, Upper boundary > 0
57e3e8
    _OPERATOR_ORDER = {"<": 2, "<=": 1, ">=": -1, ">": -2}
57e3e8
57e3e8
    def __str__(self):
57e3e8
        """Pretty-print the boundary"""
57e3e8
57e3e8
        return "{0.operator}{0.version}".format(self)
57e3e8
57e3e8
    def __compare(self, other, operator):
57e3e8
        """Compare two boundaries with provided operator.
57e3e8
57e3e8
        Boundaries compare same as (version, operator_order) tuple.
57e3e8
        In case the boundary operator is not listed in _OPERATOR_ORDER,
57e3e8
        it's order is treated as 0.
57e3e8
57e3e8
        Arguments:
57e3e8
            other (VersionBoundary): The other boundary to compare with.
57e3e8
            operator (Callable[[VersionBoundary, VersionBoundary], bool]):
57e3e8
                Comparison operator to delegate to.
57e3e8
57e3e8
        Returns:
57e3e8
            bool: The result of the operator's comparison.
57e3e8
        """
57e3e8
57e3e8
        ORDER = self._OPERATOR_ORDER
57e3e8
57e3e8
        lhs = self.version, ORDER.get(self.operator, 0)
57e3e8
        rhs = other.version, ORDER.get(other.operator, 0)
57e3e8
        return operator(lhs, rhs)
57e3e8
57e3e8
    def __eq__(self, other):
57e3e8
        return self.__compare(other, operator.eq)
57e3e8
57e3e8
    def __lt__(self, other):
57e3e8
        return self.__compare(other, operator.lt)
57e3e8
57e3e8
    def __le__(self, other):
57e3e8
        return self.__compare(other, operator.le)
57e3e8
57e3e8
    def __gt__(self, other):
57e3e8
        return self.__compare(other, operator.gt)
57e3e8
57e3e8
    def __ge__(self, other):
57e3e8
        return self.__compare(other, operator.ge)
57e3e8
57e3e8
    @property
57e3e8
    def upper(self):
57e3e8
        """True if self is upper boundary."""
57e3e8
        return self._OPERATOR_ORDER.get(self.operator, 0) > 0
57e3e8
57e3e8
    @property
57e3e8
    def lower(self):
57e3e8
        """True if self is lower boundary."""
57e3e8
        return self._OPERATOR_ORDER.get(self.operator, 0) < 0
57e3e8
57e3e8
    @classmethod
57e3e8
    def equal(cls, version):
57e3e8
        """Normalize single samp:`={version}` into equivalent x-range::
57e3e8
57e3e8
            >>> empty = VersionBoundary.equal(Version()); tuple(map(str, empty))
57e3e8
            ()
57e3e8
            >>> patch = VersionBoundary.equal(Version(1, 2, 3)); tuple(map(str, patch))
57e3e8
            ('>=1.2.3', '<1.2.4')
57e3e8
            >>> minor = VersionBoundary.equal(Version(1, 2)); tuple(map(str, minor))
57e3e8
            ('>=1.2', '<1.3')
57e3e8
            >>> major = VersionBoundary.equal(Version(1)); tuple(map(str, major))
57e3e8
            ('>=1', '<2')
57e3e8
57e3e8
        See `X-Ranges <https://docs.npmjs.com/misc/semver#x-ranges-12x-1x-12->`_
57e3e8
        for details.
57e3e8
57e3e8
        Arguments:
57e3e8
            version (Version): The version the x-range should be equal to.
57e3e8
57e3e8
        Returns:
57e3e8
            (VersionBoundary, VersionBoundary):
57e3e8
                Lower and upper bound of the x-range.
57e3e8
            (): Empty tuple in case version is empty (any version matches).
57e3e8
        """
57e3e8
57e3e8
        if version:
57e3e8
            return (
57e3e8
                cls(version=version, operator=">="),
57e3e8
                cls(version=version.incremented(), operator="<"),
57e3e8
            )
57e3e8
        else:
57e3e8
            return ()
57e3e8
57e3e8
    @classmethod
57e3e8
    def tilde(cls, version):
57e3e8
        """Normalize :samp:`~{version}` into equivalent range.
57e3e8
57e3e8
        Tilde allows patch-level changes if a minor version is specified.
57e3e8
        Allows minor-level changes if not::
57e3e8
57e3e8
            >>> with_minor = VersionBoundary.tilde(Version(1, 2, 3)); tuple(map(str, with_minor))
57e3e8
            ('>=1.2.3', '<1.3')
57e3e8
            >>> no_minor = VersionBoundary.tilde(Version(1)); tuple(map(str, no_minor))
57e3e8
            ('>=1', '<2')
57e3e8
57e3e8
        Arguments:
57e3e8
            version (Version): The version to tilde-expand.
3cc9b1
57e3e8
        Returns:
57e3e8
            (VersionBoundary, VersionBoundary):
57e3e8
                The lower and upper boundary of the tilde range.
57e3e8
        """
3cc9b1
57e3e8
        # Fail on ``~*`` or similar nonsense specifier
57e3e8
        assert version.major is not None, "Nonsense '~*' specifier"
3cc9b1
57e3e8
        lower_boundary = cls(version=version, operator=">=")
3cc9b1
57e3e8
        if version.minor is None:
57e3e8
            upper_boundary = cls(version=Version(version.major + 1), operator="<")
57e3e8
        else:
57e3e8
            upper_boundary = cls(
57e3e8
                version=Version(version.major, version.minor + 1), operator="<"
57e3e8
            )
57e3e8
57e3e8
        return lower_boundary, upper_boundary
57e3e8
57e3e8
    @classmethod
57e3e8
    def caret(cls, version):
57e3e8
        """Normalize :samp:`^{version}` into equivalent range.
57e3e8
57e3e8
        Caret allows changes that do not modify the left-most non-zero digit
57e3e8
        in the ``(major, minor, patch)`` tuple.
57e3e8
        In other words, this allows
57e3e8
        patch and minor updates for versions 1.0.0 and above,
57e3e8
        patch updates for versions 0.X >=0.1.0,
57e3e8
        and no updates for versions 0.0.X::
57e3e8
57e3e8
            >>> major = VersionBoundary.caret(Version(1, 2, 3)); tuple(map(str, major))
57e3e8
            ('>=1.2.3', '<2')
57e3e8
            >>> minor = VersionBoundary.caret(Version(0, 2, 3)); tuple(map(str, minor))
57e3e8
            ('>=0.2.3', '<0.3')
57e3e8
            >>> patch = VersionBoundary.caret(Version(0, 0, 3)); tuple(map(str, patch))
57e3e8
            ('>=0.0.3', '<0.0.4')
57e3e8
57e3e8
        When parsing caret ranges, a missing patch value desugars to the number 0,
57e3e8
        but will allow flexibility within that value,
57e3e8
        even if the major and minor versions are both 0::
57e3e8
57e3e8
            >>> rel = VersionBoundary.caret(Version(1, 2)); tuple(map(str, rel))
57e3e8
            ('>=1.2', '<2')
57e3e8
            >>> pre = VersionBoundary.caret(Version(0, 0)); tuple(map(str, pre))
57e3e8
            ('>=0.0', '<0.1')
57e3e8
57e3e8
        A missing minor and patch values will desugar to zero,
57e3e8
        but also allow flexibility within those values,
57e3e8
        even if the major version is zero::
57e3e8
57e3e8
            >>> rel = VersionBoundary.caret(Version(1)); tuple(map(str, rel))
57e3e8
            ('>=1', '<2')
57e3e8
            >>> pre = VersionBoundary.caret(Version(0)); tuple(map(str, pre))
57e3e8
            ('>=0', '<1')
57e3e8
57e3e8
        Arguments:
57e3e8
            version (Version): The version to range-expand.
57e3e8
57e3e8
        Returns:
57e3e8
            (VersionBoundary, VersionBoundary):
57e3e8
                The lower and upper boundary of caret-range.
57e3e8
        """
57e3e8
57e3e8
        # Fail on ^* or similar nonsense specifier
57e3e8
        assert len(version) != 0, "Nonsense '^*' specifier"
57e3e8
57e3e8
        lower_boundary = cls(version=version, operator=">=")
57e3e8
57e3e8
        # Increment left-most non-zero part
57e3e8
        for idx, part in enumerate(version):
57e3e8
            if part != 0:
57e3e8
                upper_version = Version(*(version[:idx] + (part + 1,)))
57e3e8
                break
57e3e8
        else:  # No non-zero found; increment last specified part
57e3e8
            upper_version = version.incremented()
57e3e8
57e3e8
        upper_boundary = cls(version=upper_version, operator="<")
57e3e8
57e3e8
        return lower_boundary, upper_boundary
57e3e8
57e3e8
    @classmethod
57e3e8
    def hyphen(cls, lower_version, upper_version):
57e3e8
        """Construct hyphen range (inclusive set)::
57e3e8
57e3e8
            >>> full = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3, 4)); tuple(map(str, full))
57e3e8
            ('>=1.2.3', '<=2.3.4')
57e3e8
57e3e8
        If a partial version is provided as the first version in the inclusive range,
57e3e8
        then the missing pieces are treated as zeroes::
57e3e8
57e3e8
            >>> part = VersionBoundary.hyphen(Version(1, 2), Version(2, 3, 4)); tuple(map(str, part))
57e3e8
            ('>=1.2', '<=2.3.4')
57e3e8
57e3e8
        If a partial version is provided as the second version in the inclusive range,
57e3e8
        then all versions that start with the supplied parts of the tuple are accepted,
57e3e8
        but nothing that would be greater than the provided tuple parts::
3cc9b1
57e3e8
            >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3)); tuple(map(str, part))
57e3e8
            ('>=1.2.3', '<2.4')
57e3e8
            >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2)); tuple(map(str, part))
57e3e8
            ('>=1.2.3', '<3')
3cc9b1
57e3e8
        Arguments:
57e3e8
            lower_version (Version): Version on the lower range boundary.
57e3e8
            upper_version (Version): Version on the upper range boundary.
3cc9b1
57e3e8
        Returns:
57e3e8
            (VersionBoundary, VersionBoundary):
57e3e8
                Lower and upper boundaries of the hyphen range.
57e3e8
        """
57e3e8
57e3e8
        lower_boundary = cls(version=lower_version, operator=">=")
57e3e8
57e3e8
        if len(upper_version) < 3:
57e3e8
            upper_boundary = cls(version=upper_version.incremented(), operator="<")
3cc9b1
        else:
57e3e8
            upper_boundary = cls(version=upper_version, operator="<=")
57e3e8
57e3e8
        return lower_boundary, upper_boundary
57e3e8
57e3e8
57e3e8
def parse_simple_seq(specifier_string):
57e3e8
    """Parse all specifiers from a space-separated string::
57e3e8
57e3e8
        >>> single = parse_simple_seq(">=1.2.3"); list(map(str, single))
57e3e8
        ['>=1.2.3']
57e3e8
        >>> multi = parse_simple_seq("~1.2.0 <1.2.5"); list(map(str, multi))
57e3e8
        ['>=1.2.0', '<1.3', '<1.2.5']
57e3e8
57e3e8
    This method implements the ``simple (' ' simple)*`` part of the grammar:
57e3e8
    https://docs.npmjs.com/misc/semver#range-grammar.
57e3e8
57e3e8
    Arguments:
57e3e8
        specifier_string (str): Space-separated string of simple version specifiers.
57e3e8
57e3e8
    Yields:
57e3e8
        VersionBoundary: Parsed boundaries.
57e3e8
    """
57e3e8
57e3e8
    # Per-operator dispatch table
57e3e8
    # API: Callable[[Version], Iterable[VersionBoundary]]
57e3e8
    handler = {
57e3e8
        ">": lambda v: [VersionBoundary(version=v, operator=">")],
57e3e8
        ">=": lambda v: [VersionBoundary(version=v, operator=">=")],
57e3e8
        "<=": lambda v: [VersionBoundary(version=v, operator="<=")],
57e3e8
        "<": lambda v: [VersionBoundary(version=v, operator="<")],
57e3e8
        "=": VersionBoundary.equal,
57e3e8
        "~": VersionBoundary.tilde,
57e3e8
        "^": VersionBoundary.caret,
57e3e8
        None: VersionBoundary.equal,
57e3e8
    }
57e3e8
57e3e8
    for match in RANGE_SPECIFIER_SIMPLE.finditer(specifier_string):
57e3e8
        operator, version_string = match.group("operator", "version")
57e3e8
57e3e8
        for boundary in handler[operator](Version.parse(version_string)):
57e3e8
            yield boundary
57e3e8
57e3e8
57e3e8
def parse_range(range_string):
57e3e8
    """Parse full NPM version range specification::
57e3e8
57e3e8
        >>> empty = parse_range(""); list(map(str, empty))
57e3e8
        []
57e3e8
        >>> simple = parse_range("^1.0"); list(map(str, simple))
57e3e8
        ['>=1.0', '<2']
57e3e8
        >>> hyphen = parse_range("1.0 - 2.0"); list(map(str, hyphen))
57e3e8
        ['>=1.0', '<2.1']
57e3e8
57e3e8
    This method implements the ``range`` part of the grammar:
57e3e8
    https://docs.npmjs.com/misc/semver#range-grammar.
57e3e8
57e3e8
    Arguments:
57e3e8
        range_string (str): The range specification to parse.
57e3e8
57e3e8
    Returns:
57e3e8
        Iterable[VersionBoundary]: Parsed boundaries.
57e3e8
57e3e8
    Raises:
57e3e8
        UnsupportedVersionToken: ``||`` is present in range_string.
57e3e8
    """
57e3e8
57e3e8
    HYPHEN = " - "
57e3e8
57e3e8
    # FIXME: rpm should be able to process OR in dependencies
57e3e8
    # This error reporting kept for backward compatibility
57e3e8
    if "||" in range_string:
57e3e8
        raise UnsupportedVersionToken(range_string)
57e3e8
57e3e8
    if HYPHEN in range_string:
57e3e8
        version_pair = map(Version.parse, range_string.split(HYPHEN, 2))
57e3e8
        return VersionBoundary.hyphen(*version_pair)
57e3e8
57e3e8
    elif range_string != "":
57e3e8
        return parse_simple_seq(range_string)
57e3e8
3cc9b1
    else:
57e3e8
        return []
57e3e8
57e3e8
57e3e8
def unify_range(boundary_iter):
57e3e8
    """Calculate largest allowed continuous version range from a set of boundaries::
57e3e8
57e3e8
        >>> unify_range([])
57e3e8
        ()
57e3e8
        >>> _ = unify_range(parse_range("=1.2.3 <2")); tuple(map(str, _))
57e3e8
        ('>=1.2.3', '<1.2.4')
57e3e8
        >>> _ = unify_range(parse_range("~1.2 <1.2.5")); tuple(map(str, _))
57e3e8
        ('>=1.2', '<1.2.5')
3cc9b1
57e3e8
    Arguments:
57e3e8
        boundary_iter (Iterable[VersionBoundary]): The version boundaries to unify.
3cc9b1
57e3e8
    Returns:
57e3e8
        (VersionBoundary, VersionBoundary):
57e3e8
            Lower and upper boundary of the unified range.
57e3e8
    """
3cc9b1
57e3e8
    # Drop boundaries with empty version
57e3e8
    boundary_iter = (
57e3e8
        boundary for boundary in boundary_iter if not boundary.version.empty
57e3e8
    )
57e3e8
57e3e8
    # Split input sequence into upper/lower boundaries
57e3e8
    lower_list, upper_list = [], []
57e3e8
    for boundary in boundary_iter:
57e3e8
        if boundary.lower:
57e3e8
            lower_list.append(boundary)
57e3e8
        elif boundary.upper:
57e3e8
            upper_list.append(boundary)
3cc9b1
        else:
57e3e8
            msg = "Unsupported boundary for unify_range: {0}".format(boundary)
57e3e8
            raise ValueError(msg)
57e3e8
57e3e8
    # Select maximum from lower boundaries and minimum from upper boundaries
57e3e8
    intermediate = (
57e3e8
        max(lower_list) if lower_list else None,
57e3e8
        min(upper_list) if upper_list else None,
57e3e8
    )
57e3e8
57e3e8
    return tuple(filter(None, intermediate))
57e3e8
57e3e8
57e3e8
def rpm_format(requirement, version_spec="*"):
57e3e8
    """Format requirement as RPM boolean dependency::
3cc9b1
57e3e8
        >>> rpm_format("nodejs(engine)")
57e3e8
        'nodejs(engine)'
57e3e8
        >>> rpm_format("npm(foo)", ">=1.0.0")
57e3e8
        'npm(foo) >= 1.0.0'
57e3e8
        >>> rpm_format("npm(bar)", "~1.2")
57e3e8
        '(npm(bar) >= 1.2 with npm(bar) < 1.3)'
3cc9b1
57e3e8
    Arguments:
57e3e8
        requirement (str): The name of the requirement.
57e3e8
        version_spec (str): The NPM version specification for the requirement.
3cc9b1
57e3e8
    Returns:
57e3e8
        str: Formatted requirement.
57e3e8
    """
3cc9b1
57e3e8
    TEMPLATE = "{name} {boundary.operator} {boundary.version!s}"
57e3e8
57e3e8
    try:
57e3e8
        boundary_tuple = unify_range(parse_range(version_spec))
57e3e8
57e3e8
    except UnsupportedVersionToken:
57e3e8
        # FIXME: Typos and print behavior kept for backward compatibility
57e3e8
        warning_lines = [
57e3e8
            "WARNING: The {requirement} dependency contains an OR (||) dependency: '{version_spec}.",
57e3e8
            "Please manually include a versioned dependency in your spec file if necessary",
57e3e8
        ]
57e3e8
        warning = "\n".join(warning_lines).format(
57e3e8
            requirement=requirement, version_spec=version_spec
57e3e8
        )
57e3e8
        print(warning, end="", file=sys.stderr)
57e3e8
57e3e8
        return requirement
57e3e8
57e3e8
    formatted = [
57e3e8
        TEMPLATE.format(name=requirement, boundary=boundary)
57e3e8
        for boundary in boundary_tuple
57e3e8
    ]
57e3e8
57e3e8
    if len(formatted) > 1:
57e3e8
        return "({0})".format(" with ".join(formatted))
57e3e8
    elif len(formatted) == 1:
57e3e8
        return formatted[0]
3cc9b1
    else:
57e3e8
        return requirement
57e3e8
57e3e8
57e3e8
def has_only_bundled_dependencies(module_dir_path):
57e3e8
    """Determines if the module contains only bundled dependencies.
57e3e8
57e3e8
    Dependencies are considered un-bundled when they are symlinks
57e3e8
    pointing outside the root module's tree.
57e3e8
57e3e8
    Arguments:
57e3e8
        module_dir_path (str):
57e3e8
            Path to the module directory (directory with ``package.json``).
57e3e8
57e3e8
    Returns:
57e3e8
        bool: True if all dependencies are bundled, False otherwise.
57e3e8
    """
57e3e8
57e3e8
    module_root_path = os.path.abspath(module_dir_path)
57e3e8
    dependency_root_path = os.path.join(module_root_path, "node_modules")
57e3e8
57e3e8
    try:
57e3e8
        dependency_path_iter = (
57e3e8
            os.path.join(dependency_root_path, basename)
57e3e8
            for basename in os.listdir(dependency_root_path)
57e3e8
        )
57e3e8
        linked_dependency_iter = (
57e3e8
            os.path.realpath(path)
57e3e8
            for path in dependency_path_iter
57e3e8
            if os.path.islink(path)
57e3e8
        )
57e3e8
        outside_dependency_iter = (
57e3e8
            path
57e3e8
            for path in linked_dependency_iter
57e3e8
            if not path.startswith(module_root_path)
57e3e8
        )
57e3e8
57e3e8
        return not any(outside_dependency_iter)
57e3e8
    except OSError:  # node_modules does not exist
57e3e8
        return False
57e3e8
57e3e8
57e3e8
def extract_dependencies(metadata_path, optional=False):
57e3e8
    """Extract all dependencies in RPM format from package metadata.
57e3e8
57e3e8
    Arguments:
57e3e8
        metadata_path (str): Path to package metadata (``package.json``).
57e3e8
        optional (bool):
57e3e8
            If True, extract ``optionalDependencies``
57e3e8
            instead of ``dependencies``.
57e3e8
57e3e8
    Yields:
57e3e8
        RPM-formatted dependencies.
57e3e8
57e3e8
    Raises:
57e3e8
        TypeError: Invalid dependency data type.
57e3e8
    """
57e3e8
57e3e8
    if has_only_bundled_dependencies(os.path.dirname(metadata_path)):
57e3e8
        return  # skip
57e3e8
57e3e8
    # Read metadata
57e3e8
    try:
57e3e8
        with open(metadata_path, mode="r") as metadata_file:
57e3e8
            metadata = json.load(metadata_file)
57e3e8
    except OSError:  # Invalid metadata file
57e3e8
        return  # skip
57e3e8
57e3e8
    # Report required NodeJS version with required dependencies
57e3e8
    if not optional:
57e3e8
        try:
57e3e8
            yield rpm_format("nodejs(engine)", metadata["engines"]["node"])
57e3e8
        except KeyError:  # NodeJS engine version unspecified
57e3e8
            yield rpm_format("nodejs(engine)")
57e3e8
57e3e8
    # Report listed dependencies
57e3e8
    kind = "optionalDependencies" if optional else "dependencies"
57e3e8
    container = metadata.get(kind, {})
57e3e8
57e3e8
    if isinstance(container, dict):
57e3e8
        for name, version_spec in container.items():
57e3e8
            yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name), version_spec)
57e3e8
57e3e8
    elif isinstance(container, list):
57e3e8
        for name in container:
57e3e8
            yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name))
57e3e8
57e3e8
    elif isinstance(container, str):
57e3e8
        yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name))
57e3e8
57e3e8
    else:
57e3e8
        raise TypeError("invalid package.json: dependencies not a valid type")
57e3e8
57e3e8
57e3e8
if __name__ == "__main__":
57e3e8
    nested = (
57e3e8
        extract_dependencies(path.strip(), optional="--optional" in sys.argv)
57e3e8
        for path in sys.stdin
57e3e8
    )
57e3e8
    flat = chain.from_iterable(nested)
57e3e8
    # Ignore parentheses around the requirements when sorting
57e3e8
    ordered = sorted(flat, key=lambda s: s.strip("()"))
57e3e8
57e3e8
    print(*ordered, sep="\n")