#!/usr/bin/python
"""
Automatic dependency generator for Node.js libraries.
Parsed from package.json. See `man npm-json` for details.
"""
# 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.
from __future__ import unicode_literals, print_function
import json
import os
import re
import sys
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
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()]
for path in paths:
if not path.endswith('package.json'):
continue
if has_all_bundled(path):
continue
fh = open(path)
metadata = json.load(fh)
fh.close()
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
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
"""
# 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):
"""
Returns the given version tuple with the last part incremented.
(1, 2, 3) -> (1, 2, 4)
(1, 2) -> (1, 3)
(1,) -> (2,)
() -> ()
"""
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):
"""
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
"""
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):
"""
Converts an NPM requirement to an equivalent list of RPM requirements.
"""
# 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()