From 1b7847ddc96224dc31f9945cc8474c6aa8cb622e Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Sep 30 2019 23:11:06 +0000 Subject: import rh-nodejs10-3.2-3.el7 --- diff --git a/SOURCES/nodejs.attr b/SOURCES/nodejs.attr index a04c30e..303badb 100644 --- a/SOURCES/nodejs.attr +++ b/SOURCES/nodejs.attr @@ -1,3 +1,3 @@ -%__nodejs10_provides %{_rpmconfigdir}/nodejs10.prov -%__nodejs10_requires %{_rpmconfigdir}/nodejs10.req -%__nodejs10_path ^/usr/lib(64)?/node_modules/[^/]+/package\\.json$ +%__rh_nodejs10_provides %{_rpmconfigdir}/rh_nodejs10.prov +%__rh_nodejs10_requires %{_rpmconfigdir}/rh_nodejs10.req +%__rh_nodejs10_path ^(/opt/rh/rh-nodejs10/root)?/usr/lib(64)?/node_modules/[^/]+/package\\.json$ diff --git a/SOURCES/nodejs.prov b/SOURCES/nodejs.prov index 5fe4498..45f5057 100755 --- a/SOURCES/nodejs.prov +++ b/SOURCES/nodejs.prov @@ -1,12 +1,8 @@ #!/usr/bin/python - -""" -Automatic provides generator for Node.js libraries. - -Taken from package.json. See `man npm-json` for details. -""" +# -*- coding: utf-8 -*- # Copyright 2012 T.C. Hollingsworth # Copyright 2017 Tomas Tomecek +# Copyright 2019 Jan Staněk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -26,73 +22,100 @@ Taken from package.json. See `man npm-json` for details. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import print_function +"""Automatic provides generator for Node.js libraries. +Metadata taken from package.json. See `man npm-json` for details. +""" + +from __future__ import print_function, with_statement + +import json import os import sys -import json +from itertools import chain, groupby -provides = set() +DEPENDENCY_TEMPLATE = "rh-nodejs10-npm(%(name)s) = %(version)s" +BUNDLED_TEMPLATE = "bundled(nodejs-%(name)s) = %(version)s" +NODE_MODULES = {"node_modules", "node_modules.bundled"} -def handle_package_json(path, bundled=False): - """ - process package.json file available on path, print RPM dependency based on name and version - """ - if not path.endswith('package.json') or not os.path.isfile(path): - return - fh = open(path) - metadata = json.load(fh) - fh.close() - - try: - if metadata['private']: - return - except KeyError: - pass - - try: - name = metadata["name"] - except KeyError: - return - try: - version = metadata["version"] - except KeyError: - return - - if bundled: - value = "bundled(nodejs-%s) = %s" % (name, version) - else: - value = "rh-nodejs10-npm(%s) = %s" % (name, version) - provides.add(value) - - -def handle_module(path, bundled): - """ - process npm module and all its bundled dependencies + +class PrivatePackage(RuntimeError): + """Private package metadata that should not be listed.""" + + +#: Something is wrong with the ``package.json`` file +_INVALID_METADATA_FILE = (IOError, PrivatePackage, KeyError) + + +def format_metadata(metadata, bundled=False): + """Format ``package.json``-like metadata into RPM dependency. + + Arguments: + metadata (dict): Package metadata, presumably read from ``package.json``. + bundled (bool): Should the bundled dependency format be used? + + Returns: + str: RPM dependency (i.e. ``npm(example) = 1.0.0``) + + Raises: + KeyError: Expected key (i.e. ``name``, ``version``) missing in metadata. + PrivatePackage: The metadata indicate private (unlisted) package. """ - handle_package_json(path, bundled=bundled) - if not os.path.isdir(path): - path = os.path.dirname(path) - node_modules_dir_candidate = os.path.join(path, "node_modules") - if os.path.isdir(node_modules_dir_candidate): - for module_path in os.listdir(node_modules_dir_candidate): - module_abs_path = os.path.join(node_modules_dir_candidate, module_path) - # skip modules which are linked against system module - if not os.path.islink(module_abs_path): - p_json_file = os.path.join(module_abs_path, "package.json") - handle_module(p_json_file, bundled=True) - - -def main(): - """ read list of package.json paths from stdin """ - paths = [path.rstrip() for path in sys.stdin.readlines()] - - for path in paths: - handle_module(path, bundled=False) - - for provide in sorted(provides): - print(provide) + # Skip private packages + if metadata.get("private", False): + raise PrivatePackage(metadata) + + template = BUNDLED_TEMPLATE if bundled else DEPENDENCY_TEMPLATE + return template % metadata -if __name__ == '__main__': - main() + +def generate_dependencies(module_path, module_dir_set=NODE_MODULES): + """Generate RPM dependency for a module and all it's dependencies. + + Arguments: + module_path (str): Path to a module directory or it's ``package.json`` + module_dir_set (set): Base names of directories to look into + for bundled dependencies. + + Yields: + str: RPM dependency for the module and each of it's (public) bundled dependencies. + + Raises: + ValueError: module_path is not valid module or ``package.json`` file + """ + + # Determine paths to root module directory and package.json + if os.path.isdir(module_path): + root_dir = module_path + elif os.path.basename(module_path) == "package.json": + root_dir = os.path.dirname(module_path) + else: # Invalid metadata path + raise ValueError("Invalid module path '%s'" % module_path) + + for dir_path, subdir_list, __ in os.walk(root_dir): + # Currently in node_modules (or similar), continue to subdirs + if os.path.basename(dir_path) in module_dir_set: + continue + + # Read and format metadata + metadata_path = os.path.join(dir_path, "package.json") + bundled = dir_path != root_dir + try: + with open(metadata_path, mode="r") as metadata_file: + metadata = json.load(metadata_file) + yield format_metadata(metadata, bundled=bundled) + except _INVALID_METADATA_FILE: + pass # Ignore + + # Only visit subdirectories in module_dir_set + subdir_list[:] = list(module_dir_set & set(subdir_list)) + + +if __name__ == "__main__": + module_paths = (path.strip() for path in sys.stdin) + provides = chain.from_iterable(generate_dependencies(m) for m in module_paths) + + # sort|uniq + for provide, __ in groupby(sorted(provides)): + print(provide) diff --git a/SOURCES/nodejs.req b/SOURCES/nodejs.req index acd96d8..76ca68f 100755 --- a/SOURCES/nodejs.req +++ b/SOURCES/nodejs.req @@ -1,12 +1,7 @@ #!/usr/bin/python - -""" -Automatic dependency generator for Node.js libraries. - -Parsed from package.json. See `man npm-json` for details. -""" - +# -*- coding: utf-8 -*- # Copyright 2012, 2013 T.C. Hollingsworth +# Copyright 2019 Jan Staněk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -26,270 +21,692 @@ Parsed from package.json. See `man npm-json` for details. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import unicode_literals, print_function +""" Automatic dependency generator for Node.js libraries. + +Metadata parsed from package.json. See `man npm-json` for details. +""" + +from __future__ import print_function, with_statement + import json +import operator import os import re import sys +from collections import namedtuple +from itertools import chain +from itertools import takewhile -def has_all_bundled(path): - # remove 'package.json' - path = os.path.dirname(path) - node_modules_dir_candidate = os.path.join(path, "node_modules") - if os.path.isdir(node_modules_dir_candidate): - modules_abs_path = map(lambda x: os.path.join(node_modules_dir_candidate, x), - os.listdir(node_modules_dir_candidate)) - any_link = any([os.path.islink(x) for x in modules_abs_path]) - return not any_link +# Python version detection +_PY2 = sys.version_info[0] <= 2 +_PY3 = sys.version_info[0] >= 3 +if _PY2: + from future_builtins import map, filter -def main(): - #npm2rpm uses functions here to write BuildRequires so don't print anything - #until the very end - deps = [] - #it's highly unlikely that we'll ever get more than one file but we handle - #this like all RPM automatic dependency generation scripts anyway - paths = [path.rstrip() for path in sys.stdin.readlines()] +#: Name format of the requirements +REQUIREMENT_NAME_TEMPLATE = "npm({name})" - for path in paths: - if not path.endswith('package.json'): - continue +#: ``simple`` product of the NPM semver grammar. +RANGE_SPECIFIER_SIMPLE = re.compile( + r""" + (?P + <= | >= | < | > | = # primitive + | ~ | \^ # tilde/caret operators + )? + \s*(?P\S+)\s* # version specifier + """, + flags=re.VERBOSE, +) - if has_all_bundled(path): - continue - fh = open(path) - metadata = json.load(fh) - fh.close() +class UnsupportedVersionToken(ValueError): + """Version specifier contains token unsupported by the parser.""" - if '--optional' in sys.argv: - deptype = 'optionalDependencies' - else: - deptype = 'dependencies' - - if deptype == 'dependencies': - #write out the node.js interpreter dependency - req = 'rh-nodejs10-nodejs(engine)' - - if 'engines' in metadata and isinstance(metadata['engines'], dict) \ - and 'node' in metadata['engines']: - deps.append(process_dep(req, metadata['engines']['node'])) - else: - deps.append(req) - - if deptype in metadata: - if isinstance(metadata[deptype], dict): - for name, version in metadata[deptype].items(): - req = 'rh-nodejs10-npm(' + name + ')' - deps.append(process_dep(req, version)) - elif isinstance(metadata[deptype], list): - for name in metadata[deptype]: - req = 'rh-nodejs10-npm(' + name + ')' - deps.append(req) - elif isinstance(metadata[deptype], str): - req = 'rh-nodejs10-npm(' + metadata[deptype] + ')' - deps.append(req) - else: - raise TypeError('invalid package.json: dependencies not a valid type') - - print('\n'.join(deps)) - -def process_dep(req, version): - """Converts an individual npm dependency into RPM dependencies""" - - #there's no way RPM can do anything like an OR dependency - if '||' in version: - sys.stderr.write("WARNING: The {0} dependency contains an ".format(req) + - "OR (||) dependency: '{0}.\nPlease manually include ".format(version) + - "a versioned dependency in your spec file if necessary") - return req - - if ' - ' in version: - version = expand_hyphen_range(version) - - deps = convert_dep(req, version) - - if len(deps) > 1: - dep = "(" + " with ".join(deps) + ")" - else: - dep = deps[0] - return dep +class Version(tuple): + """Normalized RPM/NPM version. -def parse_version(v): - """ - Parse an individual version number like 1.2.3 into a tuple. - '1.2.3' -> (1, 2, 3) - '1.2' -> (1, 2) - '1' -> (1,) - '*' -> () - This is the "partial" production in the grammar: - https://docs.npmjs.com/misc/semver#range-grammar + The version has up to 3 components – major, minor, patch. + Any part set to None is treated as unspecified. + + :: + + 1.2.3 == Version(1, 2, 3) + 1.2 == Version(1, 2) + 1 == Version(1) + * == Version() """ - # Ignore leading 'v' - v = v.lstrip('v') - parts = v.split('.', 3) - if parts[0] in ['', 'x', 'X', '*']: - return () - if len(parts) < 2 or parts[1] in ['', 'x', 'X', '*']: - return (int(parts[0]),) - if len(parts) < 3 or parts[2] in ['', 'x', 'X', '*']: - return (int(parts[0]), int(parts[1])) - # Strip off and discard any pre-release or build qualifiers at the end. - # We can get away with this, because there is no sane way to represent - # these kinds of version requirements in RPM, and we generally expect - # the distro will only carry proper releases anyway. - return (int(parts[0]), - int(parts[1]), - int(''.join(c for c in parts[2] if c.isdigit()))) - -def incremented(v): + + __slots__ = () + + #: Version part meaning 'Any' + #: ``xr`` in https://docs.npmjs.com/misc/semver#range-grammar + _PART_ANY = re.compile(r"^[xX*]$") + #: Numeric version part + #: ``nr`` in https://docs.npmjs.com/misc/semver#range-grammar + _PART_NUMERIC = re.compile(r"0|[1-9]\d*") + + def __new__(cls, *args): + """Create new version. + + Arguments: + Version components in the order of "major", "minor", "patch". + All parts are optional:: + + >>> Version(1, 2, 3) + Version(1, 2, 3) + >>> Version(1) + Version(1) + >>> Version() + Version() + + Returns: + New Version. + """ + + if len(args) > 3: + raise ValueError("Version has maximum of 3 components") + return super(Version, cls).__new__(cls, map(int, args)) + + def __repr__(self): + """Pretty debugging format.""" + + return "{0}({1})".format(self.__class__.__name__, ", ".join(map(str, self))) + + def __str__(self): + """RPM version format.""" + + return ".".join(format(part, "d") for part in self) + + @property + def major(self): + """Major version number, if any.""" + return self[0] if len(self) > 0 else None + + @property + def minor(self): + """Major version number, if any.""" + return self[1] if len(self) > 1 else None + + @property + def patch(self): + """Major version number, if any.""" + return self[2] if len(self) > 2 else None + + @property + def empty(self): + """True if the version contains nothing but zeroes.""" + return not any(self) + + @classmethod + def parse(cls, version_string): + """Parse individual version string (like ``1.2.3``) into Version. + + This is the ``partial`` production in the grammar: + https://docs.npmjs.com/misc/semver#range-grammar + + Examples:: + + >>> Version.parse("1.2.3") + Version(1, 2, 3) + >>> Version.parse("v2.x") + Version(2) + >>> Version.parse("") + Version() + + Arguments: + version_string (str): The version_string to parse. + + Returns: + Version: Parsed result. + """ + + # Ignore leading ``v``, if any + version_string = version_string.lstrip("v") + + part_list = version_string.split(".", 2) + # Use only parts up to first "Any" indicator + part_list = list(takewhile(lambda p: not cls._PART_ANY.match(p), part_list)) + + if not part_list: + return cls() + + # Strip off and discard any pre-release or build qualifiers at the end. + # We can get away with this, because there is no sane way to represent + # these kinds of version requirements in RPM, and we generally expect + # the distro will only carry proper releases anyway. + try: + part_list[-1] = cls._PART_NUMERIC.match(part_list[-1]).group() + except AttributeError: # no match + part_list.pop() + + # Extend with ``None``s at the end, if necessary + return cls(*part_list) + + def incremented(self): + """Increment the least significant part of the version:: + + >>> Version(1, 2, 3).incremented() + Version(1, 2, 4) + >>> Version(1, 2).incremented() + Version(1, 3) + >>> Version(1).incremented() + Version(2) + >>> Version().incremented() + Version() + + Returns: + Version: New incremented Version. + """ + + if len(self) == 0: + return self.__class__() + else: + args = self[:-1] + (self[-1] + 1,) + return self.__class__(*args) + + +class VersionBoundary(namedtuple("VersionBoundary", ("version", "operator"))): + """Normalized version range boundary.""" + + __slots__ = () + + #: Ordering of primitive operators. + #: Operators not listed here are handled specially; see __compare below. + #: Convention: Lower boundary < 0, Upper boundary > 0 + _OPERATOR_ORDER = {"<": 2, "<=": 1, ">=": -1, ">": -2} + + def __str__(self): + """Pretty-print the boundary""" + + return "{0.operator}{0.version}".format(self) + + def __compare(self, other, operator): + """Compare two boundaries with provided operator. + + Boundaries compare same as (version, operator_order) tuple. + In case the boundary operator is not listed in _OPERATOR_ORDER, + it's order is treated as 0. + + Arguments: + other (VersionBoundary): The other boundary to compare with. + operator (Callable[[VersionBoundary, VersionBoundary], bool]): + Comparison operator to delegate to. + + Returns: + bool: The result of the operator's comparison. + """ + + ORDER = self._OPERATOR_ORDER + + lhs = self.version, ORDER.get(self.operator, 0) + rhs = other.version, ORDER.get(other.operator, 0) + return operator(lhs, rhs) + + def __eq__(self, other): + return self.__compare(other, operator.eq) + + def __lt__(self, other): + return self.__compare(other, operator.lt) + + def __le__(self, other): + return self.__compare(other, operator.le) + + def __gt__(self, other): + return self.__compare(other, operator.gt) + + def __ge__(self, other): + return self.__compare(other, operator.ge) + + @property + def upper(self): + """True if self is upper boundary.""" + return self._OPERATOR_ORDER.get(self.operator, 0) > 0 + + @property + def lower(self): + """True if self is lower boundary.""" + return self._OPERATOR_ORDER.get(self.operator, 0) < 0 + + @classmethod + def equal(cls, version): + """Normalize single samp:`={version}` into equivalent x-range:: + + >>> empty = VersionBoundary.equal(Version()); tuple(map(str, empty)) + () + >>> patch = VersionBoundary.equal(Version(1, 2, 3)); tuple(map(str, patch)) + ('>=1.2.3', '<1.2.4') + >>> minor = VersionBoundary.equal(Version(1, 2)); tuple(map(str, minor)) + ('>=1.2', '<1.3') + >>> major = VersionBoundary.equal(Version(1)); tuple(map(str, major)) + ('>=1', '<2') + + See `X-Ranges `_ + for details. + + Arguments: + version (Version): The version the x-range should be equal to. + + Returns: + (VersionBoundary, VersionBoundary): + Lower and upper bound of the x-range. + (): Empty tuple in case version is empty (any version matches). + """ + + if version: + return ( + cls(version=version, operator=">="), + cls(version=version.incremented(), operator="<"), + ) + else: + return () + + @classmethod + def tilde(cls, version): + """Normalize :samp:`~{version}` into equivalent range. + + Tilde allows patch-level changes if a minor version is specified. + Allows minor-level changes if not:: + + >>> with_minor = VersionBoundary.tilde(Version(1, 2, 3)); tuple(map(str, with_minor)) + ('>=1.2.3', '<1.3') + >>> no_minor = VersionBoundary.tilde(Version(1)); tuple(map(str, no_minor)) + ('>=1', '<2') + + Arguments: + version (Version): The version to tilde-expand. + + Returns: + (VersionBoundary, VersionBoundary): + The lower and upper boundary of the tilde range. + """ + + # Fail on ``~*`` or similar nonsense specifier + assert version.major is not None, "Nonsense '~*' specifier" + + lower_boundary = cls(version=version, operator=">=") + + if version.minor is None: + upper_boundary = cls(version=Version(version.major + 1), operator="<") + else: + upper_boundary = cls( + version=Version(version.major, version.minor + 1), operator="<" + ) + + return lower_boundary, upper_boundary + + @classmethod + def caret(cls, version): + """Normalize :samp:`^{version}` into equivalent range. + + Caret allows changes that do not modify the left-most non-zero digit + in the ``(major, minor, patch)`` tuple. + In other words, this allows + patch and minor updates for versions 1.0.0 and above, + patch updates for versions 0.X >=0.1.0, + and no updates for versions 0.0.X:: + + >>> major = VersionBoundary.caret(Version(1, 2, 3)); tuple(map(str, major)) + ('>=1.2.3', '<2') + >>> minor = VersionBoundary.caret(Version(0, 2, 3)); tuple(map(str, minor)) + ('>=0.2.3', '<0.3') + >>> patch = VersionBoundary.caret(Version(0, 0, 3)); tuple(map(str, patch)) + ('>=0.0.3', '<0.0.4') + + When parsing caret ranges, a missing patch value desugars to the number 0, + but will allow flexibility within that value, + even if the major and minor versions are both 0:: + + >>> rel = VersionBoundary.caret(Version(1, 2)); tuple(map(str, rel)) + ('>=1.2', '<2') + >>> pre = VersionBoundary.caret(Version(0, 0)); tuple(map(str, pre)) + ('>=0.0', '<0.1') + + A missing minor and patch values will desugar to zero, + but also allow flexibility within those values, + even if the major version is zero:: + + >>> rel = VersionBoundary.caret(Version(1)); tuple(map(str, rel)) + ('>=1', '<2') + >>> pre = VersionBoundary.caret(Version(0)); tuple(map(str, pre)) + ('>=0', '<1') + + Arguments: + version (Version): The version to range-expand. + + Returns: + (VersionBoundary, VersionBoundary): + The lower and upper boundary of caret-range. + """ + + # Fail on ^* or similar nonsense specifier + assert len(version) != 0, "Nonsense '^*' specifier" + + lower_boundary = cls(version=version, operator=">=") + + # Increment left-most non-zero part + for idx, part in enumerate(version): + if part != 0: + upper_version = Version(*(version[:idx] + (part + 1,))) + break + else: # No non-zero found; increment last specified part + upper_version = version.incremented() + + upper_boundary = cls(version=upper_version, operator="<") + + return lower_boundary, upper_boundary + + @classmethod + def hyphen(cls, lower_version, upper_version): + """Construct hyphen range (inclusive set):: + + >>> full = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3, 4)); tuple(map(str, full)) + ('>=1.2.3', '<=2.3.4') + + If a partial version is provided as the first version in the inclusive range, + then the missing pieces are treated as zeroes:: + + >>> part = VersionBoundary.hyphen(Version(1, 2), Version(2, 3, 4)); tuple(map(str, part)) + ('>=1.2', '<=2.3.4') + + If a partial version is provided as the second version in the inclusive range, + then all versions that start with the supplied parts of the tuple are accepted, + but nothing that would be greater than the provided tuple parts:: + + >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2, 3)); tuple(map(str, part)) + ('>=1.2.3', '<2.4') + >>> part = VersionBoundary.hyphen(Version(1, 2, 3), Version(2)); tuple(map(str, part)) + ('>=1.2.3', '<3') + + Arguments: + lower_version (Version): Version on the lower range boundary. + upper_version (Version): Version on the upper range boundary. + + Returns: + (VersionBoundary, VersionBoundary): + Lower and upper boundaries of the hyphen range. + """ + + lower_boundary = cls(version=lower_version, operator=">=") + + if len(upper_version) < 3: + upper_boundary = cls(version=upper_version.incremented(), operator="<") + else: + upper_boundary = cls(version=upper_version, operator="<=") + + return lower_boundary, upper_boundary + + +def parse_simple_seq(specifier_string): + """Parse all specifiers from a space-separated string:: + + >>> single = parse_simple_seq(">=1.2.3"); list(map(str, single)) + ['>=1.2.3'] + >>> multi = parse_simple_seq("~1.2.0 <1.2.5"); list(map(str, multi)) + ['>=1.2.0', '<1.3', '<1.2.5'] + + This method implements the ``simple (' ' simple)*`` part of the grammar: + https://docs.npmjs.com/misc/semver#range-grammar. + + Arguments: + specifier_string (str): Space-separated string of simple version specifiers. + + Yields: + VersionBoundary: Parsed boundaries. """ - Returns the given version tuple with the last part incremented. - (1, 2, 3) -> (1, 2, 4) - (1, 2) -> (1, 3) - (1,) -> (2,) - () -> () + + # Per-operator dispatch table + # API: Callable[[Version], Iterable[VersionBoundary]] + handler = { + ">": lambda v: [VersionBoundary(version=v, operator=">")], + ">=": lambda v: [VersionBoundary(version=v, operator=">=")], + "<=": lambda v: [VersionBoundary(version=v, operator="<=")], + "<": lambda v: [VersionBoundary(version=v, operator="<")], + "=": VersionBoundary.equal, + "~": VersionBoundary.tilde, + "^": VersionBoundary.caret, + None: VersionBoundary.equal, + } + + for match in RANGE_SPECIFIER_SIMPLE.finditer(specifier_string): + operator, version_string = match.group("operator", "version") + + for boundary in handler[operator](Version.parse(version_string)): + yield boundary + + +def parse_range(range_string): + """Parse full NPM version range specification:: + + >>> empty = parse_range(""); list(map(str, empty)) + [] + >>> simple = parse_range("^1.0"); list(map(str, simple)) + ['>=1.0', '<2'] + >>> hyphen = parse_range("1.0 - 2.0"); list(map(str, hyphen)) + ['>=1.0', '<2.1'] + + This method implements the ``range`` part of the grammar: + https://docs.npmjs.com/misc/semver#range-grammar. + + Arguments: + range_string (str): The range specification to parse. + + Returns: + Iterable[VersionBoundary]: Parsed boundaries. + + Raises: + UnsupportedVersionToken: ``||`` is present in range_string. """ - if len(v) == 3: - return (v[0], v[1], v[2] + 1) - if len(v) == 2: - return (v[0], v[1] + 1) - if len(v) == 1: - return (v[0] + 1,) - if len(v) == 0: - return () - -def expand_hyphen_range(version): + + HYPHEN = " - " + + # FIXME: rpm should be able to process OR in dependencies + # This error reporting kept for backward compatibility + if "||" in range_string: + raise UnsupportedVersionToken(range_string) + + if HYPHEN in range_string: + version_pair = map(Version.parse, range_string.split(HYPHEN, 2)) + return VersionBoundary.hyphen(*version_pair) + + elif range_string != "": + return parse_simple_seq(range_string) + + else: + return [] + + +def unify_range(boundary_iter): + """Calculate largest allowed continuous version range from a set of boundaries:: + + >>> unify_range([]) + () + >>> _ = unify_range(parse_range("=1.2.3 <2")); tuple(map(str, _)) + ('>=1.2.3', '<1.2.4') + >>> _ = unify_range(parse_range("~1.2 <1.2.5")); tuple(map(str, _)) + ('>=1.2', '<1.2.5') + + Arguments: + boundary_iter (Iterable[VersionBoundary]): The version boundaries to unify. + + Returns: + (VersionBoundary, VersionBoundary): + Lower and upper boundary of the unified range. """ - Converts a hyphen range into its equivalent comparator set. - '1.2.3 - 2.3.4' -> '>=1.2.3 <=2.3.4' - https://docs.npmjs.com/misc/semver#hyphen-ranges-xyz---abc + # Drop boundaries with empty version + boundary_iter = ( + boundary for boundary in boundary_iter if not boundary.version.empty + ) + + # Split input sequence into upper/lower boundaries + lower_list, upper_list = [], [] + for boundary in boundary_iter: + if boundary.lower: + lower_list.append(boundary) + elif boundary.upper: + upper_list.append(boundary) + else: + msg = "Unsupported boundary for unify_range: {0}".format(boundary) + raise ValueError(msg) + + # Select maximum from lower boundaries and minimum from upper boundaries + intermediate = ( + max(lower_list) if lower_list else None, + min(upper_list) if upper_list else None, + ) + + return tuple(filter(None, intermediate)) + + +def rpm_format(requirement, version_spec="*"): + """Format requirement as RPM boolean dependency:: + + >>> rpm_format("nodejs(engine)") + 'nodejs(engine)' + >>> rpm_format("npm(foo)", ">=1.0.0") + 'npm(foo) >= 1.0.0' + >>> rpm_format("npm(bar)", "~1.2") + '(npm(bar) >= 1.2 with npm(bar) < 1.3)' + + Arguments: + requirement (str): The name of the requirement. + version_spec (str): The NPM version specification for the requirement. + + Returns: + str: Formatted requirement. """ - lower, upper = version.split(' - ', 1) - upper_parts = parse_version(upper) - if len(upper_parts) == 3: - return '>={} <={}'.format(lower, upper) - # Special behaviour if the upper bound is partial: - if len(upper_parts) == 2: - return '>={} <{}.{}'.format(lower, upper_parts[0], upper_parts[1] + 1) - if len(upper_parts) == 1: - return '>={} <{}'.format(lower, upper_parts[0] + 1) - if len(upper_parts) == 0: - return '>={}'.format(lower) - -def convert_dep(req, version): + + TEMPLATE = "{name} {boundary.operator} {boundary.version!s}" + + try: + boundary_tuple = unify_range(parse_range(version_spec)) + + except UnsupportedVersionToken: + # FIXME: Typos and print behavior kept for backward compatibility + warning_lines = [ + "WARNING: The {requirement} dependency contains an OR (||) dependency: '{version_spec}.", + "Please manually include a versioned dependency in your spec file if necessary", + ] + warning = "\n".join(warning_lines).format( + requirement=requirement, version_spec=version_spec + ) + print(warning, end="", file=sys.stderr) + + return requirement + + formatted = [ + TEMPLATE.format(name=requirement, boundary=boundary) + for boundary in boundary_tuple + ] + + if len(formatted) > 1: + return "({0})".format(" with ".join(formatted)) + elif len(formatted) == 1: + return formatted[0] + else: + return requirement + + +def has_only_bundled_dependencies(module_dir_path): + """Determines if the module contains only bundled dependencies. + + Dependencies are considered un-bundled when they are symlinks + pointing outside the root module's tree. + + Arguments: + module_dir_path (str): + Path to the module directory (directory with ``package.json``). + + Returns: + bool: True if all dependencies are bundled, False otherwise. """ - Converts an NPM requirement to an equivalent list of RPM requirements. + + module_root_path = os.path.abspath(module_dir_path) + dependency_root_path = os.path.join(module_root_path, "node_modules") + + try: + dependency_path_iter = ( + os.path.join(dependency_root_path, basename) + for basename in os.listdir(dependency_root_path) + ) + linked_dependency_iter = ( + os.path.realpath(path) + for path in dependency_path_iter + if os.path.islink(path) + ) + outside_dependency_iter = ( + path + for path in linked_dependency_iter + if not path.startswith(module_root_path) + ) + + return not any(outside_dependency_iter) + except OSError: # node_modules does not exist + return False + + +def extract_dependencies(metadata_path, optional=False): + """Extract all dependencies in RPM format from package metadata. + + Arguments: + metadata_path (str): Path to package metadata (``package.json``). + optional (bool): + If True, extract ``optionalDependencies`` + instead of ``dependencies``. + + Yields: + RPM-formatted dependencies. + + Raises: + TypeError: Invalid dependency data type. """ - # The version is a space-separated set of one or more comparators. - # There can be any number of comparators (even more than two) using all the - # various shortcut operators, but ultimately the comparator set is - # equivalent to a continuous range of version numbers, with an upper and - # lower bound (possibly inclusive or exclusive at each end). - - # Start by defining the range as infinite. - lower_bound = () - lower_bound_inclusive = True - upper_bound = () - upper_bound_inclusive = False - - # Helper function to narrow the lower bound to the given version, if it's - # *higher* than what we have now. - def narrow_lower(parts, inclusive): - nonlocal lower_bound, lower_bound_inclusive - if parts > lower_bound: - lower_bound = parts - lower_bound_inclusive = inclusive - elif parts == lower_bound: - if not inclusive and lower_bound_inclusive: - lower_bound_inclusive = False - # Same for the upper bound. - def narrow_upper(parts, inclusive): - nonlocal upper_bound, upper_bound_inclusive - if parts == (): - return - if upper_bound == () or parts < upper_bound: - upper_bound = parts - upper_bound_inclusive = inclusive - elif parts == upper_bound: - if not inclusive and upper_bound_inclusive: - upper_bound_inclusive = False - - # For each comparator in the set, narrow the range to match it, - # using the two helper functions. - for operator, v in re.findall(r'(<=|>=|<|>|=|\^|~)?\s*(\S+)\s*', version): - if not operator: - operator = '=' - parts = parse_version(v) - - if operator == '>': - narrow_lower(parts, False) - - elif operator == '>=': - narrow_lower(parts, True) - - elif operator == '<': - narrow_upper(parts, False) - - elif operator == '<=': - narrow_upper(parts, True) - - elif operator == '=': - narrow_lower(parts, True) - narrow_upper(incremented(parts), False) - - elif operator == '~': - narrow_lower(parts, True) - if len(parts) == 0: - pass - elif len(parts) == 1: - narrow_upper((parts[0] + 1,), False) - else: - narrow_upper((parts[0], parts[1] + 1), False) - - elif operator == '^': - narrow_lower(parts, True) - if len(parts) == 0: - pass - elif len(parts) == 1: - narrow_upper((parts[0] + 1,), False) - elif len(parts) == 2: - if parts[0] == 0: - narrow_upper((0, parts[1] + 1), False) - else: - narrow_upper((parts[0] + 1,), False) - elif len(parts) == 3: - if parts[0] == 0 and parts[1] == 0: - narrow_upper((0, 0, parts[2] + 1), False) - elif parts[0] == 0: - narrow_upper((0, parts[1] + 1), False) - else: - narrow_upper((parts[0] + 1,), False) - - # At the end, we have an upper and lower bound which satisfies all the - # comparators in the set. This is what will become our RPM version - # requirements. - - # Special case: no effective bounds. - if not lower_bound and not upper_bound: - return [req] - - # Otherwise, produce RPM requirements for the upper and lower bounds. - deps = [] - if lower_bound not in [(), (0,), (0, 0), (0, 0, 0)]: - deps.append('{} {} {}'.format( - req, - '>=' if lower_bound_inclusive else '>', - '.'.join(str(part) for part in lower_bound))) - if upper_bound != (): - deps.append('{} {} {}'.format( - req, - '<=' if upper_bound_inclusive else '<', - '.'.join(str(part) for part in upper_bound))) - return deps - -if __name__ == '__main__': - main() + + if has_only_bundled_dependencies(os.path.dirname(metadata_path)): + return # skip + + # Read metadata + try: + with open(metadata_path, mode="r") as metadata_file: + metadata = json.load(metadata_file) + except OSError: # Invalid metadata file + return # skip + + # Report required NodeJS version with required dependencies + if not optional: + try: + yield rpm_format("nodejs(engine)", metadata["engines"]["node"]) + except KeyError: # NodeJS engine version unspecified + yield rpm_format("nodejs(engine)") + + # Report listed dependencies + kind = "optionalDependencies" if optional else "dependencies" + container = metadata.get(kind, {}) + + if isinstance(container, dict): + for name, version_spec in container.items(): + yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name), version_spec) + + elif isinstance(container, list): + for name in container: + yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name)) + + elif isinstance(container, str): + yield rpm_format(REQUIREMENT_NAME_TEMPLATE.format(name=name)) + + else: + raise TypeError("invalid package.json: dependencies not a valid type") + + +if __name__ == "__main__": + nested = ( + extract_dependencies(path.strip(), optional="--optional" in sys.argv) + for path in sys.stdin + ) + flat = chain.from_iterable(nested) + # Ignore parentheses around the requirements when sorting + ordered = sorted(flat, key=lambda s: s.strip("()")) + + print(*ordered, sep="\n") diff --git a/SOURCES/nodejs_native.attr b/SOURCES/nodejs_native.attr index 0527af6..91ea775 100644 --- a/SOURCES/nodejs_native.attr +++ b/SOURCES/nodejs_native.attr @@ -1,2 +1,2 @@ -%__nodejs_native_requires %{_rpmconfigdir}/nodejs_native.req -%__nodejs_native_path ^/usr/lib.*/node_modules/.*\\.node$ +%__rh_nodejs10_native_requires %{_rpmconfigdir}/rh_nodejs10_native.req +%__rh_nodejs10_native_path ^(/opt/rh/rh-nodejs10/root)?/usr/lib.*/node_modules/.*\\.node$ diff --git a/SPECS/rh-nodejs10.spec b/SPECS/rh-nodejs10.spec index ae1e8dc..71eed01 100644 --- a/SPECS/rh-nodejs10.spec +++ b/SPECS/rh-nodejs10.spec @@ -1,19 +1,21 @@ %global scl_name_prefix rh- %global scl_name_base nodejs %global scl_name_version 10 - + %global scl %{scl_name_prefix}%{scl_name_base}%{scl_name_version} %scl_package %scl %global install_scl 1 +%global rpm_magic_name %{lua: name, _ = string.gsub(rpm.expand("%{scl_name}"), "-", "_"); print(name)} + # do not produce empty debuginfo package %global debug_package %{nil} Summary: %scl Software Collection Name: %scl_name Version: 3.2 -Release: 2%{?dist} +Release: 3%{?dist} Source1: macros.nodejs Source2: nodejs.attr @@ -58,7 +60,7 @@ Package shipping essential configuration macros to build %scl Software Collectio %package scldevel Summary: Package shipping development files for %scl - + %description scldevel Package shipping development files, especially usefull for development of packages depending on %scl Software Collection. @@ -101,12 +103,12 @@ EOF # install rpm magic install -Dpm0644 %{SOURCE1} %{buildroot}%{_root_sysconfdir}/rpm/macros.%{name} -install -Dpm0644 %{SOURCE2} %{buildroot}%{_rpmconfigdir}/fileattrs/%{name}.attr -install -pm0755 %{SOURCE3} %{buildroot}%{_rpmconfigdir}/%{name}.prov -install -pm0755 %{SOURCE4} %{buildroot}%{_rpmconfigdir}/%{name}.req +install -Dpm0644 %{SOURCE2} %{buildroot}%{_rpmconfigdir}/fileattrs/%{rpm_magic_name}.attr +install -pm0755 %{SOURCE3} %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}.prov +install -pm0755 %{SOURCE4} %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}.req install -pm0755 %{SOURCE5} %{buildroot}%{_rpmconfigdir}/%{name}-symlink-deps install -pm0755 %{SOURCE6} %{buildroot}%{_rpmconfigdir}/%{name}-fixdep -install -Dpm0644 %{SOURCE7} %{buildroot}%{_rpmconfigdir}/fileattrs/%{name}_native.attr +install -Dpm0644 %{SOURCE7} %{buildroot}%{_rpmconfigdir}/fileattrs/%{rpm_magic_name}_native.attr install -Dpm0644 %{SOURCE10} %{buildroot}%{_datadir}/node/multiver_modules install -pm0755 %{SOURCE11} %{buildroot}%{_rpmconfigdir}/%{name}-symlink-deps @@ -117,12 +119,12 @@ EOF # ensure Requires are added to every native module that match the Provides from # the nodejs build in the buildroot -cat << EOF > %{buildroot}%{_rpmconfigdir}/%{name}_native.req +cat << EOF > %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}_native.req #!/bin/sh echo 'nodejs10-nodejs(abi) = %nodejs_abi' echo 'nodejs10-nodejs(v8-abi) = %v8_abi' EOF -chmod 0755 %{buildroot}%{_rpmconfigdir}/%{name}_native.req +chmod 0755 %{buildroot}%{_rpmconfigdir}/%{rpm_magic_name}_native.req cat << EOF > %{buildroot}%{_rpmconfigdir}/%{name}-require.sh #!/bin/sh @@ -151,6 +153,19 @@ install -m 644 %{scl_name}.7 %{buildroot}%{_mandir}/man7/%{scl_name}.7 mkdir -p %{buildroot}%{_datadir}/licenses/ +%check +# Assert that installed file attributes are named appropriately +for attr_file in %{buildroot}%{_rpmconfigdir}/fileattrs/*.attr; do + macro_name="$(basename "$attr_file"|sed -E 's|\.attr$||')" + + # Delete comments and empty lines + # Select all remaining lines that start unexpectedly + # Fail if any such line is found, cotinue otherwise + sed -E -e '/^$/d;/^#/d' "$attr_file" \ + | grep -E -v "^%%__${macro_name}_" \ + && exit 1 || : +done + %files %files -f filesystem runtime @@ -169,13 +184,17 @@ mkdir -p %{buildroot}%{_datadir}/licenses/ %files build %{_root_sysconfdir}/rpm/macros.%{scl}-config %{_root_sysconfdir}/rpm/macros.%{name} -%{_rpmconfigdir}/fileattrs/%{name}*.attr +%{_rpmconfigdir}/fileattrs/%{rpm_magic_name}*.attr %{_rpmconfigdir}/%{name}* +%{_rpmconfigdir}/%{rpm_magic_name}* %files scldevel %{_root_sysconfdir}/rpm/macros.%{scl_name_base}-scldevel %changelog +* Fri Sep 06 2019 Jan Staněk - 3.2-3 +- Import enhanced Provides/Requires generators from Fedora + * Thu Sep 20 2018 Zuzana Svetlikova - 3.2-2 - Resolves: RHBZ#1584252 - update to fedora packaging, generate bundled provides automaticaly @@ -244,7 +263,7 @@ mkdir -p %{buildroot}%{_datadir}/licenses/ - Own python modules directory * Wed Oct 08 2014 Tomas Hrcka - 1.2-29 -- Require scriptlet scl_devel from root_bindir not scl_bindir +- Require scriptlet scl_devel from root_bindir not scl_bindir * Mon Oct 06 2014 Tomas Hrcka - 1.2-28 - bump scl version @@ -261,10 +280,10 @@ mkdir -p %{buildroot}%{_datadir}/licenses/ Related: #1061452 * Mon Feb 17 2014 Tomas Hrcka - 1.1-24 -- Require version of scl-utils +- Require version of scl-utils * Wed Feb 12 2014 Tomas Hrcka - 1.1-23 -- Define scl_name_base and scl_name_version macros +- Define scl_name_base and scl_name_version macros * Wed Feb 12 2014 Honza Horak - 1.1-22 - Some more grammar fixes in README @@ -287,7 +306,7 @@ mkdir -p %{buildroot}%{_datadir}/licenses/ - clean up after previous fix * Fri Aug 09 2013 thrcka@redhat.com - 1-16 -- RHBZ#993425 - nodejs010.req fails when !noarch +- RHBZ#993425 - nodejs010.req fails when !noarch * Mon Jun 03 2013 thrcka@redhat.com - 1-15 - Changed licence to MIT