diff --git a/.gitignore b/.gitignore index 5ea9eaa..9756798 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,14 @@ +*~ +*.swp +__pycache__/ +*.pyc +nodejs_req.py +test/*/package.json +test/*/nodejs.prov.err +test/*/nodejs.prov.out +test/*/nodejs.req.err +test/*/nodejs.req.out *.rpm .build-*.log noarch/ -nodejs-packaging-fedora-*/ -/nodejs-packaging-fedora-2.tar.xz -/nodejs-packaging-fedora-3.tar.xz -/nodejs-packaging-fedora-4.tar.xz -/nodejs-packaging-fedora-6.tar.xz -/nodejs-packaging-fedora-7.tar.xz -/nodejs-packaging-fedora-8.tar.xz -/nodejs-packaging-fedora-9.tar.xz -/nodejs-packaging-fedora-10.tar.xz -/nodejs-packaging-fedora-11.tar.xz -/nodejs-packaging-fedora-12.tar.xz -/nodejs-packaging-fedora-13.tar.xz -/nodejs-packaging-fedora-14.tar.xz -/nodejs-packaging-fedora-15.tar.xz -/nodejs-packaging-fedora-16.tar.xz -/nodejs-packaging-fedora-17.tar.xz -/nodejs-packaging-fedora-18.tar.xz -/nodejs-packaging-fedora-19.tar.xz -/nodejs-packaging-fedora-20.tar.xz -/nodejs-packaging-fedora-21.tar.xz -/nodejs-packaging-fedora-22.tar.xz -/nodejs-packaging-fedora-23.tar.xz -/nodejs-packaging-fedora-24.tar.xz -/nodejs-packaging-fedora-25.tar.xz +/test.tar.gz diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3792306 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2012, 2013 T.C. Hollingsworth + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c45063 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# How to update Node.js in Fedora + +## Determine the Node.js version +Monitor the [Node.js Blog](https://nodejs.org/en/blog/) to be notified of +available updates. + +For simplicity and copy-and-paste of instructions below, set some variables +here: + +``` +NODEJS_MAJOR=12 +NODEJS_VERSION=12.9.0 +``` + +## Clone the Fedora package repository +These steps assume that you are a comaintainer of Node.js or a provenpackager +in Fedora. + +``` +fedpkg clone nodejs nodejs-fedora +``` + +Next, switch to the major version branch you are going to update. We'll use +Node.js 12.9.0 in this document. Adjust the versions appropriately for the +version you are working on. + +``` +pushd nodejs-fedora +fedpkg switch-branch $NODEJS_MAJOR +popd +``` + + +## Clone the Fedora Module repository + +``` +fedpkg clone modules/nodejs nodejs-fedora-module +``` + + +## Clone the upstream Node.js repository +``` +git clone -o upstream git://github.com/nodejs/node.git nodejs-upstream +``` + + +## Rebase the Fedora patches atop the latest release + +``` +pushd nodejs-upstream +git checkout -b fedora-v$NODEJS_VERSION v$NODEJS_VERSION +git am -3 ../nodejs-fedora/*.patch +``` + +If the patches do not apply cleanly, resolve the merges appropriately. Once +they have all been applied, output them again: + +``` +git format-patch -M --patience --full-index -o ../nodejs-fedora v$NODEJS_VERSION..HEAD +popd +``` + + +## Update the Node.js tarball and specfile + +``` +pushd nodejs-fedora +./nodejs-tarball.sh $NODEJS_VERSION +``` + +Note that this command will also output all of the versions for the software +bundled with Node.js. You will need to edit `nodejs.spec` and update the +%global values near the top of that file to include the appropriate values +matching the dependencies. Make sure to also update the Node.js versions too! + +Note that if libuv is updated, you need to ensure that the libuv in each +buildroot is of a sufficient version. If not, you may need to update that +package first and submit a buildroot override. + +Update the RPM spec %changelog appropriately. + + +## (Preferred) Perform a scratch-build on at least one architecture + +``` +fedpkg scratch-build [--arches x86_64] --srpm +``` + +Verify that it built successfully. + + +## Push the changes up to Fedora +``` +fedpkg commit -cs +fedpkg push +popd +``` + + +## (Optional) Build for Fedora releases + +If this major version is the default for one or more Fedora releases, build it +for them. (Note: this step will go away in the future, once module default +streams are available in the non-modular buildroot.) + +In the case of Node.js 12.x, this is the default version for Fedora 31 and 32. + +``` +pushd nodejs-fedora +fedpkg switch-branch [master|31] +git merge $NODEJS_MAJOR +fedpkg push +fedpkg build +popd +``` + +## Build module stream + +``` +pushd nodejs-fedora-module +fedpkg switch-branch $NODEJS_MAJOR +``` + +If the module has changed any package dependencies (such as added a dep on a +new shared library), you may need to modify nodejs.yaml here. If not, you can +simply run: + +``` +git commit --allow-empty -sm "Update to $NODEJS_VERSION" +fedpkg push +fedpkg module-build +popd +``` + +## Submit built packages to Bodhi +Follow the usual processes for stable/branched releases to submit builds for +testing. diff --git a/macros.nodejs b/macros.nodejs new file mode 100644 index 0000000..f86891c --- /dev/null +++ b/macros.nodejs @@ -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} diff --git a/multiver_modules b/multiver_modules new file mode 100644 index 0000000..2dc44eb --- /dev/null +++ b/multiver_modules @@ -0,0 +1,3 @@ +uglify-js +inherits +nan \ No newline at end of file diff --git a/nodejs-fixdep b/nodejs-fixdep new file mode 100755 index 0000000..2569266 --- /dev/null +++ b/nodejs-fixdep @@ -0,0 +1,117 @@ +#!/usr/bin/python3 + +"""Modify a dependency listed in a package.json file""" + +# Copyright 2013 T.C. Hollingsworth +# +# 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() diff --git a/nodejs-packaging.spec b/nodejs-packaging.spec index e15f2ca..1292b9c 100644 --- a/nodejs-packaging.spec +++ b/nodejs-packaging.spec @@ -1,15 +1,28 @@ %global macrosdir %(d=%{_rpmconfigdir}/macros.d; [ -d $d ] || d=%{_sysconfdir}/rpm; echo $d) Name: nodejs-packaging -Version: 25 +Version: 2020.09 Release: 1%{?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 +Source0001: LICENSE +Source0002: README.md +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 + +# Created with `tar cfz test.tar.gz test` +Source0101: test.tar.gz + BuildRequires: python3 Requires: redhat-rpm-config @@ -20,7 +33,10 @@ Node.js modules and applications in RPM-based distributions. %prep -%autosetup -p 1 -n %{name}-fedora-%{version} +pushd %{_topdir}/BUILD +cp -da %{_sourcedir}/* . +tar xvf test.tar.gz +popd %build @@ -51,6 +67,9 @@ install -Dpm0644 multiver_modules %{buildroot}%{_datadir}/node/multiver_modules %changelog +* Fri Sep 18 2020 Stephen Gallagher - 2020.09-1 +- Move to dist-git as the upstream + * Wed Sep 02 2020 Stephen Gallagher - 25-1 - Fix incorrect bundled library detection for Requires diff --git a/nodejs-setversion b/nodejs-setversion new file mode 100755 index 0000000..49bbe1e --- /dev/null +++ b/nodejs-setversion @@ -0,0 +1,43 @@ +#!/usr/bin/python3 + +"""Set a package version in a package.json file""" + +# Copyright 2018 Tom Hughes +# +# 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() diff --git a/nodejs-symlink-deps b/nodejs-symlink-deps new file mode 100755 index 0000000..b5e44b3 --- /dev/null +++ b/nodejs-symlink-deps @@ -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 +# +# 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: + +""".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: + + +It is generally reccomended to remove the entire "node_modules" directory in +%prep when it exists. For more information, see: + + +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) diff --git a/nodejs.attr b/nodejs.attr new file mode 100644 index 0000000..1066499 --- /dev/null +++ b/nodejs.attr @@ -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$ diff --git a/nodejs.prov b/nodejs.prov new file mode 100755 index 0000000..663d3d9 --- /dev/null +++ b/nodejs.prov @@ -0,0 +1,121 @@ +#!/usr/bin/python3 +# -*- 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 +# 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, __ 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/nodejs.req b/nodejs.req new file mode 100755 index 0000000..129606b --- /dev/null +++ b/nodejs.req @@ -0,0 +1,707 @@ +#!/usr/bin/python3 +# -*- 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 +# 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 + <= | >= | < | > | = # primitive + | ~ | \^ # tilde/caret operators + )? + \s*(?P\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 `_ + 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") diff --git a/sources b/sources index a462a2e..52e9e27 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (nodejs-packaging-fedora-25.tar.xz) = ba82999a2ac41114f2925be7a8fe9602e9ac17474b4c4b29de888f2eec9f9ca4ec7fad0e830f763426e1c5026b212bd1d2ddc1d19dbad74ac13fb77d36f85b80 +SHA512 (test.tar.gz) = dfbda67b8741f1ca36bf63b2e842f81ba07381b3d92e75fa7e29f8e456543b4cae55e95785902f31a14ae4d0b7e89161ba04c0c10c2fff617b4ae9607c91e599 diff --git a/test/bundled/node_modules/test100/package.json b/test/bundled/node_modules/test100/package.json new file mode 100644 index 0000000..5e1a529 --- /dev/null +++ b/test/bundled/node_modules/test100/package.json @@ -0,0 +1,4 @@ +{ + "name": "test100", + "version": "1.3.5" +} diff --git a/test/bundled/node_modules/test101/package.json b/test/bundled/node_modules/test101/package.json new file mode 100644 index 0000000..f98ae32 --- /dev/null +++ b/test/bundled/node_modules/test101/package.json @@ -0,0 +1,4 @@ +{ + "name": "test101", + "version": "2.1.4" +} diff --git a/test/bundled/nodejs.prov.err.exp b/test/bundled/nodejs.prov.err.exp new file mode 100644 index 0000000..e69de29 diff --git a/test/bundled/nodejs.prov.out.exp b/test/bundled/nodejs.prov.out.exp new file mode 100644 index 0000000..ee0f024 --- /dev/null +++ b/test/bundled/nodejs.prov.out.exp @@ -0,0 +1,3 @@ +bundled(nodejs-test100) = 1.3.5 +bundled(nodejs-test101) = 2.1.4 +npm(test) = 4.5.6 diff --git a/test/bundled/nodejs.req.err.exp b/test/bundled/nodejs.req.err.exp new file mode 100644 index 0000000..e69de29 diff --git a/test/bundled/nodejs.req.out.exp b/test/bundled/nodejs.req.out.exp new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/bundled/nodejs.req.out.exp @@ -0,0 +1 @@ + diff --git a/test/bundled/package.json.in b/test/bundled/package.json.in new file mode 100644 index 0000000..e41f38b --- /dev/null +++ b/test/bundled/package.json.in @@ -0,0 +1,11 @@ +{ + "name": "test", + "version": "4.5.6", + "engines": { + "node": ">=6 <10" + }, + "dependencies": { + "test100": "^1.2.3", + "test101": ">=2.1" + } +} diff --git a/test/run b/test/run new file mode 100755 index 0000000..6afb356 --- /dev/null +++ b/test/run @@ -0,0 +1,18 @@ +#!/bin/sh + +ln -sf nodejs.req nodejs_req.py +"$(command -v python2 || echo :)" -m doctest nodejs_req.py || exit 1 +"$(command -v python3 || echo :)" -m doctest nodejs_req.py || exit 1 + +for test in unbundled bundled +do + sed -e "s|//.*$||" < test/$test/package.json.in > test/$test/package.json + + echo test/$test/package.json | ./nodejs.prov test/$test/package.json > test/$test/nodejs.prov.out 2> test/$test/nodejs.prov.err + diff -uw test/$test/nodejs.prov.err.exp test/$test/nodejs.prov.err || exit 1 + diff -uw test/$test/nodejs.prov.out.exp test/$test/nodejs.prov.out || exit 1 + + echo test/$test/package.json | ./nodejs.req test/$test/package.json > test/$test/nodejs.req.out 2> test/$test/nodejs.req.err + diff -uw test/$test/nodejs.req.err.exp test/$test/nodejs.req.err || exit 1 + diff -uw test/$test/nodejs.req.out.exp test/$test/nodejs.req.out || exit 1 +done diff --git a/test/unbundled/nodejs.prov.err.exp b/test/unbundled/nodejs.prov.err.exp new file mode 100644 index 0000000..e69de29 diff --git a/test/unbundled/nodejs.prov.out.exp b/test/unbundled/nodejs.prov.out.exp new file mode 100644 index 0000000..2ea2883 --- /dev/null +++ b/test/unbundled/nodejs.prov.out.exp @@ -0,0 +1 @@ +npm(test) = 4.5.6 diff --git a/test/unbundled/nodejs.req.err.exp b/test/unbundled/nodejs.req.err.exp new file mode 100644 index 0000000..83bd277 --- /dev/null +++ b/test/unbundled/nodejs.req.err.exp @@ -0,0 +1,2 @@ +WARNING: The npm(test900) dependency contains an OR (||) dependency: '^1.2 || ^2.2. +Please manually include a versioned dependency in your spec file if necessary diff --git a/test/unbundled/nodejs.req.out.exp b/test/unbundled/nodejs.req.out.exp new file mode 100644 index 0000000..08aeab1 --- /dev/null +++ b/test/unbundled/nodejs.req.out.exp @@ -0,0 +1,74 @@ +(nodejs(engine) >= 6 with nodejs(engine) < 10) +(npm(test100) >= 1 with npm(test100) < 2) +(npm(test101) >= 1 with npm(test101) < 2) +(npm(test102) >= 1 with npm(test102) < 2) +(npm(test103) >= 1 with npm(test103) < 2) +(npm(test104) >= 1.2 with npm(test104) < 1.3) +(npm(test105) >= 1.2 with npm(test105) < 1.3) +(npm(test106) >= 1.2 with npm(test106) < 1.3) +(npm(test107) >= 1.2 with npm(test107) < 1.3) +(npm(test108) >= 1.2.3 with npm(test108) < 1.2.4) +(npm(test109) >= 1.2.3 with npm(test109) < 1.2.4) +(npm(test110) >= 1.2.3 with npm(test110) < 1.2.4) +(npm(test111) >= 1.2.3 with npm(test111) < 1.2.4) +npm(test200) > 1 +npm(test201) > 1.2 +npm(test202) > 1.2.3 +npm(test203) >= 1 +npm(test204) >= 1.2 +npm(test205) >= 1.2.3 +npm(test206) < 2 +npm(test207) < 2.3 +npm(test208) < 2.3.4 +npm(test209) <= 2 +npm(test210) <= 2.3 +npm(test211) <= 2.3.4 +(npm(test300) > 1 with npm(test300) < 2) +(npm(test301) > 1.2 with npm(test301) < 2.3) +(npm(test302) > 1.2.3 with npm(test302) < 2.3.4) +(npm(test303) >= 1 with npm(test303) <= 2) +(npm(test304) >= 1.2 with npm(test304) <= 2.3) +(npm(test305) >= 1.2.3 with npm(test305) <= 2.3.4) +(npm(test306) > 1 with npm(test306) < 2) +(npm(test307) > 1.2 with npm(test307) < 2.3) +(npm(test308) > 1.2.3 with npm(test308) < 2.3.4) +(npm(test309) >= 1 with npm(test309) <= 2) +(npm(test310) >= 1.2 with npm(test310) <= 2.3) +(npm(test311) >= 1.2.3 with npm(test311) <= 2.3.4) +(npm(test400) >= 1.2.3 with npm(test400) <= 2.3.4) +(npm(test401) >= 1.2.3 with npm(test401) < 2.4) +(npm(test402) >= 1.2.3 with npm(test402) < 3) +(npm(test403) >= 1.2 with npm(test403) <= 2.3.4) +(npm(test404) >= 1 with npm(test404) <= 2.3.4) +(npm(test405) >= 1.2 with npm(test405) < 2.4) +(npm(test406) >= 1.2 with npm(test406) < 3) +(npm(test407) >= 1 with npm(test407) < 2.4) +(npm(test408) >= 1 with npm(test408) < 3) +(npm(test500) >= 1.2 with npm(test500) < 1.3) +(npm(test501) >= 1.2 with npm(test501) < 1.3) +(npm(test502) >= 1 with npm(test502) < 2) +(npm(test503) >= 1 with npm(test503) < 2) +npm(test504) +npm(test505) +(npm(test600) >= 1.2.3 with npm(test600) < 1.3) +(npm(test601) >= 1.2 with npm(test601) < 1.3) +(npm(test602) >= 1.2 with npm(test602) < 1.3) +(npm(test603) >= 1 with npm(test603) < 2) +(npm(test604) >= 1 with npm(test604) < 2) +(npm(test700) >= 1.2.3 with npm(test700) < 2) +(npm(test701) >= 0.2.3 with npm(test701) < 0.3) +(npm(test702) >= 0.0.3 with npm(test702) < 0.0.4) +(npm(test703) >= 1.2 with npm(test703) < 2) +(npm(test704) >= 1.2 with npm(test704) < 2) +(npm(test705) >= 0.1 with npm(test705) < 0.2) +(npm(test706) >= 0.1 with npm(test706) < 0.2) +(npm(test707) >= 1 with npm(test707) < 2) +(npm(test708) >= 1 with npm(test708) < 2) +npm(test709) < 0.1 +npm(test710) < 0.1 +npm(test711) < 1 +npm(test712) < 1 +npm(test750) >= 0.10 +(npm(test751) >= 0.10 with npm(test751) <= 6) +(npm(test800) > 1.2 with npm(test800) < 1.9) +npm(test900) diff --git a/test/unbundled/package.json.in b/test/unbundled/package.json.in new file mode 100644 index 0000000..4d9624e --- /dev/null +++ b/test/unbundled/package.json.in @@ -0,0 +1,108 @@ +{ + "name": "test", + "version": "4.5.6", + "engines": { + "node": ">=6 <10" + }, + "dependencies": { + // Single version + "test100": "1", + "test101": "=1", + "test102": "v1", + "test103": "=v1", + "test104": "1.2", + "test105": "=1.2", + "test106": "v1.2", + "test107": "=v1.2", + "test108": "1.2.3", + "test109": "=1.2.3", + "test110": "v1.2.3", + "test111": "=v1.2.3", + + // Ranges with one comparator + "test200": ">1", + "test201": ">1.2", + "test202": ">1.2.3", + "test203": ">=1", + "test204": ">=1.2", + "test205": ">=1.2.3", + "test206": "<2", + "test207": "<2.3", + "test208": "<2.3.4", + "test209": "<=2", + "test210": "<=2.3", + "test211": "<=2.3.4", + + // Ranges with two comparators + "test300": ">1 <2", + "test301": ">1.2 <2.3", + "test302": ">1.2.3 <2.3.4", + "test303": ">=1 <=2", + "test304": ">=1.2 <=2.3", + "test305": ">=1.2.3 <=2.3.4", + "test306": "<2 >1", + "test307": "<2.3 >1.2", + "test308": "<2.3.4 >1.2.3", + "test309": "<=2 >=1", + "test310": "<=2.3 >=1.2", + "test311": "<=2.3.4 >=1.2.3", + + // Hyphen ranges + "test400": "1.2.3 - 2.3.4", + "test401": "1.2.3 - 2.3", + "test402": "1.2.3 - 2", + "test403": "1.2 - 2.3.4", + "test404": "1 - 2.3.4", + "test405": "1.2 - 2.3", + "test406": "1.2 - 2", + "test407": "1 - 2.3", + "test408": "1 - 2", + + // X-Ranges + "test500": "1.2.x", + "test501": "1.2.*", + "test502": "1.x", + "test503": "1.*", + "test504": "*", + "test505": "", + + // Tilde ranges + "test600": "~1.2.3", + "test601": "~1.2.x", + "test602": "~1.2", + "test603": "~1.x", + "test604": "~1", + + // Caret ranges + "test700": "^1.2.3", + "test701": "^0.2.3", + "test702": "^0.0.3", + "test703": "^1.2.x", + "test704": "^1.2", + "test705": "^0.1.x", + "test706": "^0.1", + "test707": "^1.x", + "test708": "^1", + "test709": "^0.0.x", + "test710": "^0.0", + "test711": "^0.x", + "test712": "^0", + + // Space after the operator + // (the grammar does not permit this, but it is accepted in practice) + "test750": ">= 0.10", + "test751": ">= 0.10 <= 6", + + // More than two comparators in a set + // (no reason for this to ever appear, but it is permitted) + "test800": ">1.2 <2.0 <1.9", + + // The following cases are not implemented currently... + + // Multiple comparator sets separated by || + "test900": "^1.2 || ^2.2" + + // The whole pre-release stuff: https://docs.npmjs.com/misc/semver//prerelease-tags + // which is not even enumerated here because it is so complex. + } +}