import CS nodejs-packaging-2021.06-4.module_el9+583+597f83c2

This commit is contained in:
eabdullin 2023-10-06 12:15:17 +00:00
parent 74e3b1a602
commit 5fa236438e
13 changed files with 1365 additions and 17 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
SOURCES/nodejs-packaging-fedora-23.tar.xz
SOURCES/test.tar.gz

View File

@ -1 +1 @@
7fa84f4a8aa409eca9694d062eed8eaa8f879345 SOURCES/nodejs-packaging-fedora-23.tar.xz
dfaa3308397a75e566baec53cc32e6a635eca1e9 SOURCES/test.tar.gz

19
SOURCES/LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright 2012, 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

37
SOURCES/macros.nodejs Normal file
View File

@ -0,0 +1,37 @@
# nodejs binary
%__nodejs %{_bindir}/node
# nodejs library directory
%nodejs_sitelib %{_prefix}/lib/node_modules
#arch specific library directory
#for future-proofing only; we don't do multilib
%nodejs_sitearch %{nodejs_sitelib}
# currently installed nodejs version
%nodejs_version %(%{__nodejs} -v | sed s/v//)
# symlink dependencies so `npm link` works
# this should be run in every module's %%install section
# pass --check to work in the current directory instead of the buildroot
# pass --no-devdeps to ignore devDependencies when --check is used
%nodejs_symlink_deps %{_rpmconfigdir}/nodejs-symlink-deps %{nodejs_sitelib}
# patch package.json to fix a dependency
# see `man npm-json` for details on writing dependencies for package.json files
# e.g. `%%nodejs_fixdep frobber` makes any version of frobber do
# `%%nodejs_fixdep frobber '>1.0'` requires frobber > 1.0
# `%%nodejs_fixdep -r frobber removes the frobber dep
%nodejs_fixdep %{_rpmconfigdir}/nodejs-fixdep
# patch package.json to set the package version
# e.g. `%%nodejs_setversion 1.2.3`
%nodejs_setversion %{_rpmconfigdir}/nodejs-setversion
# macro to filter unwanted provides from Node.js binary native modules
%nodejs_default_filter %{expand: \
%global __provides_exclude_from ^%{nodejs_sitearch}/.*\\.node$
}
# no-op macro to allow spec compatibility with EPEL
%nodejs_find_provides_and_requires %{nil}

3
SOURCES/multiver_modules Normal file
View File

@ -0,0 +1,3 @@
uglify-js
inherits
nan

117
SOURCES/nodejs-fixdep Executable file
View File

@ -0,0 +1,117 @@
#!/usr/bin/python3
"""Modify a dependency listed in a package.json file"""
# Copyright 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import json
import optparse
import os
import re
import shutil
import sys
RE_VERSION = re.compile(r'\s*v?([<>=~^]{0,2})\s*([0-9][0-9\.\-]*)\s*')
p = optparse.OptionParser(
description='Modifies dependency entries in package.json files')
p.add_option('-r', '--remove', action='store_true')
p.add_option('-m', '--move', action='store_true')
p.add_option('--dev', action='store_const', const='devDependencies',
dest='deptype', help='affect devDependencies')
p.add_option('--optional', action='store_const', const='optionalDependencies',
dest='deptype', help='affect optionalDependencies')
p.add_option('--caret', action='store_true',
help='convert all or specified dependencies to use the caret operator')
options, args = p.parse_args()
if not os.path.exists('package.json~'):
shutil.copy2('package.json', 'package.json~')
md = json.load(open('package.json'))
deptype = options.deptype if options.deptype is not None else 'dependencies'
if deptype not in md:
md[deptype] = {}
# convert alternate JSON dependency representations to a dictionary
if not options.caret and not isinstance(md[deptype], dict):
if isinstance(md[deptype], list):
deps = md[deptype]
md[deptype] = {}
for dep in deps:
md[deptype][dep] = '*'
elif isinstance(md[deptype], str):
md[deptype] = { md[deptype] : '*' }
if options.remove:
dep = args[0]
del md[deptype][dep]
elif options.move:
dep = args[0]
ver = None
for fromtype in ['dependencies', 'optionalDependencies', 'devDependencies']:
if fromtype in md:
if isinstance(md[fromtype], dict) and dep in md[fromtype]:
ver = md[fromtype][dep]
del md[fromtype][dep]
elif isinstance(md[fromtype], list) and md[fromtype].count(dep) > 0:
ver = '*'
md[fromtype].remove(dep)
elif isinstance(md[fromtype], str) and md[fromtype] == dep:
ver = '*'
del md[fromtype]
if ver != None:
md[deptype][dep] = ver
elif options.caret:
if not isinstance(md[deptype], dict):
sys.stderr.write('All dependencies are unversioned. Unable to apply ' +
'caret operator.\n')
sys.exit(2)
deps = args if len(args) > 0 else md[deptype].keys()
for dep in deps:
if md[deptype][dep][0] == '^':
continue
elif md[deptype][dep][0] in ('~','0','1','2','3','4','5','6','7','8','9'):
ver = re.match(RE_VERSION, md[deptype][dep]).group(2)
md[deptype][dep] = '^' + ver
else:
sys.stderr.write('Attempted to convert non-numeric or tilde ' +
'dependency to caret. This is not permitted.\n')
sys.exit(1)
else:
dep = args[0]
if len(args) > 1:
ver = args[1]
else:
ver = '*'
md[deptype][dep] = ver
fh = open('package.json', 'w')
data = json.JSONEncoder(indent=4).encode(md)
fh.write(data)
fh.close()

View File

@ -0,0 +1,95 @@
#!/bin/bash
OUTPUT_DIR="$(rpm -E '%{_sourcedir}')"
usage() {
echo "Usage `basename $0` <npm_name> [version] " >&2
echo >&2
echo " Given a npm module name, and optionally a version," >&2
echo " download the npm, the prod and dev dependencies," >&2
echo " each in their own tarball." >&2
echo " Also finds licenses prod dependencies." >&2
echo " All three tarballs and the license list are copied to ${OUTPUT_DIR}" >&2
echo >&2
exit 1
}
if ! [ -f /usr/bin/npm ]; then
echo >&2
echo "`basename $0` requires npm to run" >&2
echo >&2
echo "Run the following to fix this" >&2
echo " sudo dnf install npm" >&2
echo >&2
exit 2
fi
if [ $# -lt 1 ]; then
usage
else
case $1 in
-h | --help )
usage
;;
* )
PACKAGE="$1"
;;
esac
fi
if [ $# -ge 2 ]; then
VERSION="$2"
else
VERSION="$(npm view ${PACKAGE} version)"
fi
# the package name might contain invalid characters, sanitize first
PACKAGE_SAFE=$(echo $PACKAGE | sed -e 's|/|-|g')
TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX)
mkdir -p ${OUTPUT_DIR}
mkdir -p ${TMP_DIR}
pushd ${TMP_DIR}
npm pack ${PACKAGE}
tar xfz *.tgz
cd package
echo " Downloading prod dependencies"
npm install --no-optional --only=prod
if [ $? -ge 1 ] ; then
echo " ERROR WILL ROBINSON"
rm -rf node_modules
else
echo " Successful prod dependencies download"
mv node_modules/ node_modules_prod
fi
echo "LICENSES IN BUNDLE:"
find . -name "package.json" -exec jq '.license | strings' {} \; >> ${TMP_DIR}/${PACKAGE_SAFE}-${VERSION}-bundled-licenses.txt
find . -name "package.json" -exec jq '.license | objects | .type' {} \; >> ${TMP_DIR}/${PACKAGE_SAFE}-${VERSION}-bundled-licenses.txt 2>/dev/null
find . -name "package.json" -exec jq '.licenses[] .type' {} \; >> ${TMP_DIR}/${PACKAGE_SAFE}-${VERSION}-bundled-licenses.txt 2>/dev/null
sort -u -o ${TMP_DIR}/${PACKAGE_SAFE}-${VERSION}-bundled-licenses.txt ${TMP_DIR}/${PACKAGE_SAFE}-${VERSION}-bundled-licenses.txt
# Locate any dependencies without a provided license
find . -type f -name package.json -execdir jq 'if .license==null and .licenses==null then .name else null end' '{}' '+' | grep -vE '^null$' | sort -u > ${TMP_DIR}/nolicense.txt
if [ -s ${TMP_DIR}/nolicense.txt ]; then
echo -e "\e[5m\e[41mSome dependencies do not list a license. Manual verification required!\e[0m"
cat ${TMP_DIR}/nolicense.txt
echo -e "\e[5m\e[41m======================================================================\e[0m"
fi
echo " Downloading dev dependencies"
npm install --no-optional --only=dev
if [ $? -ge 1 ] ; then
echo " ERROR WILL ROBINSON"
else
echo " Successful dev dependencies download"
mv node_modules/ node_modules_dev
fi
if [ -d node_modules_prod ] ; then
tar cfz ../${PACKAGE_SAFE}-${VERSION}-nm-prod.tgz node_modules_prod
fi
if [ -d node_modules_dev ] ; then
tar cfz ../${PACKAGE_SAFE}-${VERSION}-nm-dev.tgz node_modules_dev
fi
cd ..
cp -v ${PACKAGE_SAFE}-${VERSION}* "${OUTPUT_DIR}"
popd > /dev/null
rm -rf ${TMP_DIR}

43
SOURCES/nodejs-setversion Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/python3
"""Set a package version in a package.json file"""
# Copyright 2018 Tom Hughes <tom@compton.nu>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import json
import os
import shutil
import sys
if not os.path.exists('package.json~'):
shutil.copy2('package.json', 'package.json~')
md = json.load(open('package.json'))
if 'version' in md and sys.argv[1] != md['version']:
raise RuntimeError('Version is already set to {0}'.format(md['version']))
else:
md['version'] = sys.argv[1]
fh = open('package.json', 'w')
data = json.JSONEncoder(indent=4).encode(md)
fh.write(data)
fh.close()

141
SOURCES/nodejs-symlink-deps Executable file
View File

@ -0,0 +1,141 @@
#!/usr/bin/python3
"""Symlink a node module's dependencies into the node_modules directory so users
can `npm link` RPM-installed modules into their personal projects."""
# Copyright 2012, 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import json
import os
import re
import shutil
import sys
def symlink(source, dest):
try:
os.symlink(source, dest)
except OSError:
if os.path.islink(dest) and os.path.realpath(dest) == os.path.normpath(source):
sys.stderr.write("""
WARNING: the symlink for dependency "{0}" already exists
This could mean that the dependency exists in both devDependencies and
dependencies, which may cause trouble for people using this module with npm.
Please report this to upstream. For more information, see:
<https://github.com/tchollingsworth/nodejs-packaging/pull/1>
""".format(dest))
elif '--force' in sys.argv:
if os.path.isdir(dest):
shutil.rmtree(dest)
else:
os.unlink(dest)
os.symlink(source, dest)
else:
sys.stderr.write("""
ERROR: the path for dependency "{0}" already exists
This could mean that bundled modules are being installed. Bundled libraries are
forbidden in Fedora. For more information, see:
<https://fedoraproject.org/wiki/Packaging:No_Bundled_Libraries>
It is generally reccomended to remove the entire "node_modules" directory in
%prep when it exists. For more information, see:
<https://fedoraproject.org/wiki/Packaging:Node.js#Removing_bundled_modules>
If you have obtained permission from the Fedora Packaging Committee to bundle
libraries, please use `%nodejs_fixdep -r` in %prep to remove the dependency on
the bundled module. This will prevent an unnecessary dependency on the system
version of the module and eliminate this error.
""".format(dest))
sys.exit(1)
def symlink_deps(deps, check):
if isinstance(deps, dict):
#read in the list of mutiple-versioned packages
mvpkgs = open('/usr/share/node/multiver_modules').read().split('\n')
for dep, ver in deps.items():
if dep in mvpkgs and ver != '' and ver != '*' and ver != 'latest':
depver = re.sub('^ *(~|\^|=|>=|<=) *', '', ver).split('.')[0]
target = os.path.join(sitelib, '{0}@{1}'.format(dep, depver))
else:
target = os.path.join(sitelib, dep)
if not check or os.path.exists(target):
symlink(target, dep)
elif isinstance(deps, list):
for dep in deps:
target = os.path.join(sitelib, dep)
if not check or os.path.exists(target):
symlink(target, dep)
elif isinstance(deps, str):
target = os.path.join(sitelib, deps)
if not check or os.path.exists(target):
symlink(target, deps)
else:
raise TypeError("Invalid package.json: dependencies weren't a recognized type")
#the %nodejs_symlink_deps macro passes %nodejs_sitelib as the first argument
sitelib = sys.argv[1]
if '--check' in sys.argv or '--build' in sys.argv:
check = True
modules = [os.getcwd()]
else:
check = False
br_sitelib = os.path.join(os.environ['RPM_BUILD_ROOT'], sitelib.lstrip('/'))
modules = [os.path.join(br_sitelib, module) for module in os.listdir(br_sitelib)]
if '--optional' in sys.argv:
optional = True
else:
optional = False
for path in modules:
os.chdir(path)
md = json.load(open('package.json'))
if 'dependencies' in md or (check and 'devDependencies' in md) or (optional and 'optionalDependencies' in md):
try:
os.mkdir('node_modules')
except OSError:
sys.stderr.write('WARNING: node_modules already exists. Make sure you have ' +
'no bundled dependencies.\n')
os.chdir('node_modules')
if 'dependencies' in md:
symlink_deps(md['dependencies'], check)
if check and '--no-devdeps' not in sys.argv and 'devDependencies' in md:
symlink_deps(md['devDependencies'], check)
if optional and 'optionalDependencies' in md:
symlink_deps(md['optionalDependencies'], check)

4
SOURCES/nodejs.attr Normal file
View File

@ -0,0 +1,4 @@
%__nodejs_provides %{_rpmconfigdir}/nodejs.prov
%__nodejs_requires %{_rpmconfigdir}/nodejs.req
%__nodejs_suggests %{_rpmconfigdir}/nodejs.req --optional
%__nodejs_path ^/usr/lib(64)?/node_modules/[^/]+/package\\.json$

121
SOURCES/nodejs.prov Executable file
View File

@ -0,0 +1,121 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright 2012 T.C. Hollingsworth <tchollingsworth@gmail.com>
# Copyright 2017 Tomas Tomecek <ttomecek@redhat.com>
# Copyright 2019 Jan Staněk <jstanek@redhat.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
"""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
from itertools import chain, groupby
DEPENDENCY_TEMPLATE = "npm(%(name)s) = %(version)s"
BUNDLED_TEMPLATE = "bundled(nodejs-%(name)s) = %(version)s"
NODE_MODULES = {"node_modules", "node_modules_prod"}
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.
"""
# Skip private packages
if metadata.get("private", False):
raise PrivatePackage(metadata)
template = BUNDLED_TEMPLATE if bundled else DEPENDENCY_TEMPLATE
return template % metadata
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, file_list in os.walk(root_dir):
# We are only interested in directories that contain package.json
if "package.json" not in file_list:
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)

707
SOURCES/nodejs.req Executable file
View File

@ -0,0 +1,707 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Copyright 2012, 2013 T.C. Hollingsworth <tchollingsworth@gmail.com>
# Copyright 2019 Jan Staněk <jstanek@redat.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
""" 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
# Python version detection
_PY2 = sys.version_info[0] <= 2
_PY3 = sys.version_info[0] >= 3
if _PY2:
from future_builtins import map, filter
#: Name format of the requirements
REQUIREMENT_NAME_TEMPLATE = "npm({name})"
#: ``simple`` product of the NPM semver grammar.
RANGE_SPECIFIER_SIMPLE = re.compile(
r"""
(?P<operator>
<= | >= | < | > | = # primitive
| ~ | \^ # tilde/caret operators
)?
\s*(?P<version>\S+)\s* # version specifier
""",
flags=re.VERBOSE,
)
class UnsupportedVersionToken(ValueError):
"""Version specifier contains token unsupported by the parser."""
class Version(tuple):
"""Normalized RPM/NPM version.
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()
"""
__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 <https://docs.npmjs.com/misc/semver#x-ranges-12x-1x-12->`_
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.
"""
# 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.
"""
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.
"""
# 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.
"""
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.
"""
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)
)
bundled_dependency_iter = (
os.path.realpath(path)
for path in dependency_path_iter
if not os.path.islink(path) or path.startswith(module_root_path)
)
return any(bundled_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.
"""
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")

View File

@ -1,28 +1,54 @@
%global macrosdir %(d=%{_rpmconfigdir}/macros.d; [ -d $d ] || d=%{_sysconfdir}/rpm; echo $d)
Name: nodejs-packaging
Version: 23
Release: 3%{?dist}
Version: 2021.06
Release: 4%{?dist}
Summary: RPM Macros and Utilities for Node.js Packaging
BuildArch: noarch
License: MIT
URL: https://fedoraproject.org/wiki/Node.js/Packagers
Source0: https://releases.pagure.org/%{name}/%{name}-fedora-%{version}.tar.xz
ExclusiveArch: %{nodejs_arches} noarch
BuildRequires: python3-devel
Source0001: LICENSE
Source0003: macros.nodejs
Source0004: multiver_modules
Source0005: nodejs-fixdep
Source0006: nodejs-setversion
Source0007: nodejs-symlink-deps
Source0008: nodejs.attr
Source0009: nodejs.prov
Source0010: nodejs.req
Source0011: nodejs-packaging-bundler
# Created with `tar cfz test.tar.gz test`
Source0101: test.tar.gz
BuildRequires: python3
#nodejs-devel before 0.10.12 provided these macros and owned /usr/share/node
Recommends: nodejs(engine) >= 0.10.12
Requires: redhat-rpm-config
%description
This package contains RPM macros and other utilities useful for packaging
Node.js modules and applications in RPM-based distributions.
%package bundler
Summary: Bundle a node.js application dependencies
Requires: npm
Requires: coreutils, findutils, jq
%description bundler
nodejs-packaging-bundler bundles a node.js application node_module dependencies
It gathers the application tarball.
It generates a runtime (prod) tarball with runtime node_module dependencies
It generates a testing (dev) tarball with node_module dependencies for testing
It generates a bundled license file that gets the licenses in the runtime
dependency tarball
%prep
%autosetup -p 1 -n %{name}-fedora-%{version}
pushd %{_topdir}/BUILD
cp -da %{_sourcedir}/* .
tar xvf test.tar.gz
popd
%build
@ -38,6 +64,7 @@ install -pm0755 nodejs-symlink-deps %{buildroot}%{_rpmconfigdir}/nodejs-symlink-
install -pm0755 nodejs-fixdep %{buildroot}%{_rpmconfigdir}/nodejs-fixdep
install -pm0755 nodejs-setversion %{buildroot}%{_rpmconfigdir}/nodejs-setversion
install -Dpm0644 multiver_modules %{buildroot}%{_datadir}/node/multiver_modules
install -Dpm0755 nodejs-packaging-bundler %{buildroot}%{_bindir}/nodejs-packaging-bundler
%check
@ -51,11 +78,48 @@ install -Dpm0644 multiver_modules %{buildroot}%{_datadir}/node/multiver_modules
%{_rpmconfigdir}/nodejs*
%{_datadir}/node/multiver_modules
%files bundler
%{_bindir}/nodejs-packaging-bundler
%changelog
* Wed May 06 2020 Zuzana Svetlikova <zsvetlik@redhat.com> - 23-3
- Updated
- Removed pathfix.py
* Thu Jan 20 2022 Stephen Gallagher <sgallagh@redhat.com> - 2021.06-4
- NPM bundler: also find namespaced bundled dependencies
* Thu Jul 22 2021 Fedora Release Engineering <releng@fedoraproject.org> - 2021.06-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
* Tue Jun 22 2021 Stephen Gallagher <sgallagh@redhat.com> - 2021.06-2
- Fix hard-coded output directory in the bundler
* Wed Jun 02 2021 Stephen Gallagher <sgallagh@redhat.com> - 2021.06-1
- Update to 2021.06-1
- bundler: Handle archaic license metadata
- bundler: Warn about bundled dependencies with no license metadata
* Tue Jan 26 2021 Fedora Release Engineering <releng@fedoraproject.org> - 2021.01-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild
* Wed Jan 20 2021 Stephen Gallagher <sgallagh@redhat.com> - 2021.01-2
- nodejs-packaging-bundler improvements to handle uncommon characters
* Wed Jan 06 2021 Troy Dawson <tdawson@redhat.com> - 2021.01
- Add nodejs-packaging-bundler and update README.md
* Fri Sep 18 2020 Stephen Gallagher <sgallagh@redhat.com> - 2020.09-1
- Move to dist-git as the upstream
* Wed Sep 02 2020 Stephen Gallagher <sgallagh@redhat.com> - 25-1
- Fix incorrect bundled library detection for Requires
* Tue Sep 01 2020 Stephen Gallagher <sgallagh@redhat.com> - 24-1
- Check node_modules_prod for bundled dependencies
* Tue Jul 28 2020 Fedora Release Engineering <releng@fedoraproject.org> - 23-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild
* Wed Jun 03 2020 Stephen Gallagher <sgallagh@redhat.com> - 23-3
- Drop Requires: nodejs(engine)
* Wed Jan 29 2020 Fedora Release Engineering <releng@fedoraproject.org> - 23-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
@ -82,11 +146,8 @@ install -Dpm0644 multiver_modules %{buildroot}%{_datadir}/node/multiver_modules
* Thu Jan 3 2019 Tom Hughes <tom@compton.nu> - 18-1
- Handle =, >= and <= dependencies for multiversion modules
* Tue Jan 1 2019 zsvetlik@redhat.com - 17-3
- Change Requires to Recommends on nodejs dependency, so it is usable for building nodejs
* Wed Jul 04 2018 Tomas Orsava <torsava@redhat.com> - 17-2
- Switch hardcoded python3 shebangs into the %%{__python3} macro
* Fri Jul 13 2018 Fedora Release Engineering <releng@fedoraproject.org> - 17-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
* Thu May 3 2018 Tom Hughes <tom@compton.nu> - 17-1
- Fix version comparators with a space after the operator