Import rpm: 072c286fc5ce5539a266627ee0199304d0dc83be

This commit is contained in:
James Antill 2023-02-20 02:00:09 -05:00
commit 76e164f38e
34 changed files with 1909 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
SOURCES/nodejs-packaging-fedora-17.tar.xz

19
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.

218
README.md Normal file
View File

@ -0,0 +1,218 @@
# 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 [--arch 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.
# How to bundle nodejs libraries in Fedora
The upstream Node.js stance on
[global library packages](https://nodejs.org/en/blog/npm/npm-1-0-global-vs-local-installation/)
is that they are ".. best avoided if not needed." In Fedora, we take the same
stance with our nodejs packages. You can provide a package that uses nodejs,
but you should bundle all the nodejs libraries that are needed.
We are providing a sample spec file and bundling script here.
For more detailed packaging information go to the
[Fedora Node.js Packaging Guildelines](https://docs.fedoraproject.org/en-US/packaging-guidelines/Node.js/)
## Bundling Script
```
nodejs-packaging-bundler <npm_name> [version]
```
nodejs-packaging-bundler is it's own package, nodejs-packaging-bundler and must be installed before use.
nodejs-packaging-bundler gets the latest npm version available, if no version is given.
It produces four files and puts them in ${HOME}/rpmbuild/SOURCES
* <npm_name>-<version>.tgz - This is the tarball from npm.org
* <npm_name>-<version>-nm-prod.tgz - This is the tarball that contains all the bundled nodejs modules <npm_name> needs to run
* <npm_name>-<version>-nm-dev.tgz - This is the tarball that contains all the bundled nodejs modules <npm_name> needs to test
* <npm_name>-<version>-bundled-licenses.txt - This lists the bundled licenses in <npm_name>-<version>-nm-prod.tgz
## Sample Spec File
```
%global npm_name my_nodejs_application
...
License: <license1> and <license2> and <license3>
...
Source0: http://registry.npmjs.org/%{npm_name}/-/%{npm_name}-%{version}.tgz
Source1: %{npm_name}-%{version}-nm-prod.tgz
Source2: %{npm_name}-%{version}-nm-dev.tgz
Source3: %{npm_name}-%{version}-bundled-licenses.txt
...
BuildRequires: nodejs-devel
...
%prep
%setup -q -n package
cp %{SOURCE3} .
...
%build
# Setup bundled node modules
tar xfz %{SOURCE1}
mkdir -p node_modules
pushd node_modules
ln -s ../node_modules_prod/* .
ln -s ../node_modules_prod/.bin .
popd
...
%install
mkdir -p %{buildroot}%{nodejs_sitelib}/%{npm_name}
cp -pr index.js lib package.json %{buildroot}%{nodejs_sitelib}/%{npm_name}
# Copy over bundled nodejs modules
cp -pr node_modules node_modules_prod %{buildroot}%{nodejs_sitelib}/%{npm_name}
...
%check
%nodejs_symlink_deps --check
# Setup bundled dev node_modules for testing
tar xfz %{SOURCE2}
pushd node_modules
ln -s ../node_modules_dev/* .
popd
pushd node_modules/.bin
ln -s ../../node_modules_dev/.bin/* .
popd
# Example test run using the binary in ./node_modules/.bin/
./node_modules/.bin/vows --spec --isolate
...
%files
%doc HISTORY.md
%license LICENSE.md %{npm_name}-%{version}-bundled-licenses.txt
%{nodejs_sitelib}/%{npm_name}
```

37
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
multiver_modules Normal file
View File

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

117
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()

95
nodejs-packaging-bundler Executable file
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}

153
nodejs-packaging.spec Normal file
View File

@ -0,0 +1,153 @@
%global macrosdir %(d=%{_rpmconfigdir}/macros.d; [ -d $d ] || d=%{_sysconfdir}/rpm; echo $d)
Name: nodejs-packaging
Version: 17
Release: 3%{?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
#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.
%prep
%autosetup -p 1 -n %{name}-fedora-%{version}
# Modify the shebangs
pathfix.py -pni "%{__python3}" \
nodejs-fixdep nodejs-setversion nodejs.prov nodejs.req nodejs-symlink-deps
%build
#nothing to do
%install
install -Dpm0644 macros.nodejs %{buildroot}%{macrosdir}/macros.nodejs
install -Dpm0644 nodejs.attr %{buildroot}%{_rpmconfigdir}/fileattrs/nodejs.attr
install -pm0755 nodejs.prov %{buildroot}%{_rpmconfigdir}/nodejs.prov
install -pm0755 nodejs.req %{buildroot}%{_rpmconfigdir}/nodejs.req
install -pm0755 nodejs-symlink-deps %{buildroot}%{_rpmconfigdir}/nodejs-symlink-deps
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
%check
./test/run
%files
%license LICENSE
%{macrosdir}/macros.nodejs
%{_rpmconfigdir}/fileattrs/nodejs*.attr
%{_rpmconfigdir}/nodejs*
%{_datadir}/node/multiver_modules
%changelog
* Tue Mar 12 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
* Thu May 3 2018 Tom Hughes <tom@compton.nu> - 17-1
- Fix version comparators with a space after the operator
* Tue May 1 2018 Tom Hughes <tom@compton.nu> - 16-1
- Rewrite nodejs.req to better match npm versioning rules
- Add tests for nodejs.req and nodejs.prov
* Mon Apr 30 2018 Tom Hughes <tom@compton.nu> - 15-1
- Fix caret dependency ranges
* Thu Apr 12 2018 Tom Hughes <tom@compton.nu> - 14-1
- Only match top level modules for requires and provides generation
* Wed Feb 28 2018 Tom Hughes <tom@compton.nu> - 13-1
- Add %%nodejs_setversion macro
* Fri Feb 23 2018 Tom Hughes <tom@compton.nu> - 12-1
- Port to python 3
* Thu Feb 08 2018 Fedora Release Engineering <releng@fedoraproject.org> - 11-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
* Sat Jan 13 2018 Tom Hughes <tom@compton.nu> - 11-1
- nodesjs.req: use boolean with for range dependencies
* Tue Sep 12 2017 Stephen Gallagher <sgallagh@redhat.com> - 10-1
- Release v10
- Automatically generate Provides for bundled npm dependencies
* Thu Jul 27 2017 Fedora Release Engineering <releng@fedoraproject.org> - 9-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
* Tue Feb 21 2017 Tom Hughes <tom@compton.nu> - 9-3
- switch source URL to pagure
* Fri Feb 10 2017 Fedora Release Engineering <releng@fedoraproject.org> - 9-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild
* Tue Feb 2 2016 Tom Hughes <tom@compton.nu> - 9-1
- nodejs-fixdep: stop --move erroring on missing dependency types
* Sun Jan 31 2016 Tom Hughes <tom@compton.nu> - 8-1
- nodejs-fixdep: add --move option
- nodejs-symlink-deps: add --optional option
- req: generate suggests for optional dependencies
* Mon Nov 16 2015 Tom Hughes <tom@compton.nu> - 7-5
- nodejs-symlink-deps: handle caret in versions
* Wed Jun 17 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 7-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild
* Wed Mar 4 2015 Ville Skyttä <ville.skytta@iki.fi> - 7-3
- Install macros in %%{_rpmconfidir}/macros.d where available (#1074279)
* Sat Jun 07 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 7-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild
* Sun May 25 2014 T.C. Hollingsworth <tchollingsworth@gmail.com> - 7-1
- nodejs-symlink-deps: fix regression preventing multiply versioned modules from
being symlinked correctly
* Sat May 24 2014 T.C. Hollingsworth <tchollingsworth@gmail.com> - 6-1
- nodejs-fixdep: use real option parsing
- nodejs-fixdep: support modifying optionalDependencies and devDependencies
- req: support the caret operator
- nodejs-symlink-deps: add --force option
- nodejs-symlink-deps: add --build alias for --check
- nodejs-fixdep: support converting to caret dependencies
- nodejs-fixdep: support non-dictionary dependency properties
- multiver_modules: add nan
* Mon Jul 29 2013 T.C. Hollingsworth <tchollingsworth@gmail.com> - 4-1
- handle cases where the symlink target exists gracefully
* Wed Jul 10 2013 T.C. Hollingsworth <tchollingsworth@gmail.com> - 3-1
- dependencies and engines can be lists or strings too
- handle unversioned dependencies on multiply versioned modules correctly
(RHBZ#982798)
- restrict to compatible arches
* Fri Jun 21 2013 T.C. Hollingsworth <tchollingsworth@gmail.com> - 2-1
- move multiple version list to /usr/share/node
- bump nodejs Requires to 0.10.12
- add Requires: redhat-rpm-config
* Thu Jun 13 2013 T.C. Hollingsworth <tchollingsworth@gmail.com> - 1-1
- initial package

43
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
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
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
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
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")

1
sources Normal file
View File

@ -0,0 +1 @@
SHA1 (nodejs-packaging-fedora-17.tar.xz) = da522c9640b73b1df7a3522bbec96c0475fc7cd2

4
test/bundled/node_modules/test100/package.json generated vendored Normal file
View File

@ -0,0 +1,4 @@
{
"name": "test100",
"version": "1.3.5"
}

4
test/bundled/node_modules/test101/package.json generated vendored Normal file
View File

@ -0,0 +1,4 @@
{
"name": "test101",
"version": "2.1.4"
}

View File

View File

@ -0,0 +1,3 @@
bundled(nodejs-test100) = 1.3.5
bundled(nodejs-test101) = 2.1.4
npm(test) = 4.5.6

View File

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,11 @@
{
"name": "test",
"version": "4.5.6",
"engines": {
"node": ">=6 <10"
},
"dependencies": {
"test100": "^1.2.3",
"test101": ">=2.1"
}
}

View File

@ -0,0 +1,4 @@
{
"name": "@nmcli/test201",
"version": "2.1.4"
}

View File

@ -0,0 +1,4 @@
{
"name": "test200",
"version": "1.3.5"
}

View File

@ -0,0 +1,3 @@
bundled(nodejs-@nmcli/test201) = 2.1.4
bundled(nodejs-test200) = 1.3.5
npm(test) = 4.5.6

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,11 @@
{
"name": "test",
"version": "4.5.6",
"engines": {
"node": ">=6 <10"
},
"dependencies": {
"test100": "^1.2.3",
"test101": ">=2.1"
}
}

18
test/run Executable file
View File

@ -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 bundled_namespace
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

View File

View File

@ -0,0 +1 @@
npm(test) = 4.5.6

View File

@ -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

View File

@ -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)

View File

@ -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.
}
}