From 25b10bd1a93e12a7e49fee38b0a229281ae49fb7 Mon Sep 17 00:00:00 2001
From: Jakub Jirutka <jakub@jirutka.cz>
Date: Sun, 11 Apr 2021 00:33:39 +0200
Subject: [PATCH] main/nodejs: move npm into a standalone aport

npm is bundled in Node.js, but it's a standalone project with its
own release cycle and version number. main/nodejs provides LTS
version of Node.js, so it includes old version of npm.

Alpine build tools don't handle subpackages with pkgver different
from the origin pkgver. Thus the current 'npm' subpackage has version
14.16.1-r0 (version of the Node.js) which is confusing, because the
real version of the packaged 'npm' is 6.14.11.

Moreover, npm has gazillion bundled dependencies, so there's a high
risk of security vulnerabilities; using npm bundled in Node.js
quite complicates security patching and requires rebuilding complete
Node.js package.

For these reasons, I think it will be better to split npm into a
separate aport and provide the latest version instead of some arbitrary
version bundled in the Node.js tarball.

Actually, I planned this three years ago (see commit message in
244cc743c4ae2fd0f517b74790674864cb293e9c), but forgot about it.

There's one unpleasant consequence of this change - the latest npm
version is 7.9.0 which is lower than 14.16.1 (version inherited from
nodejs package). Since Alpine doesn't have "epoch" version as e.g.
Fedora, there's nothing I can do about it beside informing the users
(using nodejs.post-upgrade script).
---
 main/nodejs/APKBUILD                          |  39 ++----
 main/nodejs/nodejs.post-upgrade               |  26 ++++
 main/npm/APKBUILD                             | 116 ++++++++++++++++++
 main/npm/dont-check-for-last-version.patch    |  18 +++
 main/npm/make-dont-install-deps.patch         |  11 ++
 main/npm/npmrc                                |   6 +
 .../reproducible-documentation-build.patch    |  20 +++
 main/npm/smoke-tests-npm-location.patch       |  23 ++++
 8 files changed, 229 insertions(+), 30 deletions(-)
 create mode 100644 main/nodejs/nodejs.post-upgrade
 create mode 100644 main/npm/APKBUILD
 create mode 100644 main/npm/dont-check-for-last-version.patch
 create mode 100644 main/npm/make-dont-install-deps.patch
 create mode 100644 main/npm/npmrc
 create mode 100644 main/npm/reproducible-documentation-build.patch
 create mode 100644 main/npm/smoke-tests-npm-location.patch

diff --git a/main/nodejs/APKBUILD b/main/nodejs/APKBUILD
index 9e52df1a0716..bb1b6648af4c 100644
--- a/main/nodejs/APKBUILD
+++ b/main/nodejs/APKBUILD
@@ -68,7 +68,7 @@ pkgname=nodejs
 # Note: Update only to even-numbered versions (e.g. 6.y.z, 8.y.z)!
 # Odd-numbered versions are supported only for 9 months by upstream.
 pkgver=14.16.1
-pkgrel=0
+pkgrel=1
 pkgdesc="JavaScript runtime built on V8 engine - LTS version"
 url="https://nodejs.org/"
 arch="all !mips64 !mips64el"
@@ -84,7 +84,8 @@ makedepends="
 	python3
 	zlib-dev
 	"
-subpackages="$pkgname-dev $pkgname-doc npm::noarch"
+install="$pkgname.post-upgrade"
+subpackages="$pkgname-dev $pkgname-doc"
 provides="nodejs-lts=$pkgver"  # for backward compatibility
 replaces="nodejs-current nodejs-lts"  # nodejs-lts for backward compatibility
 source="https://nodejs.org/dist/v$pkgver/node-v$pkgver.tar.gz
@@ -121,6 +122,10 @@ build() {
 	# couldn't upgrade nodejs package in stable branches to fix CVEs due to
 	# libuv incompatibility.
 	#
+	# NOTE: We don't package the bundled npm - it's a separate project with
+	# its own release cycle and version numbering, so it's better to keep
+	# it in a standalone aport.
+	#
 	# TODO: After icu package is modified to split data into multiple
 	# variants, change --with-intl to "system-icu".
 	python3 configure.py --prefix=/usr \
@@ -132,7 +137,8 @@ build() {
 		--shared-nghttp2 \
 		--openssl-use-def-ca-store \
 		--with-icu-default-data-dir=$(icu-config --icudatadir) \
-		--with-intl=small-icu
+		--with-intl=small-icu \
+		--without-npm
 
 	make BUILDTYPE=Release
 }
@@ -147,19 +153,6 @@ check() {
 
 package() {
 	make DESTDIR="$pkgdir" install
-
-	cp -pr "$pkgdir"/usr/lib/node_modules/npm/man "$pkgdir"/usr/share
-	local d; for d in docs man; do
-		rm -r "$pkgdir"/usr/lib/node_modules/npm/$d
-	done
-
-	# XXX: Workaround for https://github.com/npm/cli/issues/780.
-	(cd "$pkgdir"/usr/share/man/man5 && find * \
-		-type f ! \( -name 'package-json.*' -or -name 'npmrc.*' -or -name 'npm-*' \) \
-		-exec mv {} npm-{} \;)
-	(cd "$pkgdir"/usr/share/man/man7 && find * \
-		-type f ! \( -name 'semver.*' -or -name 'npm-*' \) \
-		-exec mv {} npm-{} \;)
 }
 
 dev() {
@@ -167,20 +160,6 @@ dev() {
 	default_dev
 }
 
-npm() {
-	pkgdesc="A package manager for JavaScript"
-	depends="$pkgname"
-	# for backward compatibility
-	provides="nodejs-npm=$pkgver-r$pkgrel nodejs-current-npm=$pkgver-r$pkgrel"
-	replaces="nodejs-npm nodejs-current-npm $pkgname"
-
-	mkdir -p "$subpkgdir"/usr/bin
-	mv "$pkgdir"/usr/bin/np[mx] "$subpkgdir"/usr/bin/
-
-	mkdir -p "$subpkgdir"/usr/lib/node_modules
-	mv "$pkgdir"/usr/lib/node_modules/npm "$subpkgdir"/usr/lib/node_modules/
-}
-
 sha512sums="40843674584c2010958b4faf12290b525f3e5b13d37e52e3b41d50691de16cc0a29ed1fbc81912a0f76f48648c603dfb726242d232e4542f46ab957a4042c05d  node-v14.16.1.tar.gz
 dbe8167b61518f8f59176759d69834d57bf3e6a5a5fd3dfc2359cafe0325da08b27f8220d278ed77f50c9f63a03313eabbbb0eaca3e592e5bb4e0d5be0ced373  disable-running-gyp-on-shared-deps.patch
 44e81fbf254bd79e38b813f7f5a1336df854588939cba50aaec600660495f9b7745a7049a99eb59d15a51100b3a44f66892a902d7fc32e1399b51883ad4c02cf  link-with-libatomic-on-mips32.patch"
diff --git a/main/nodejs/nodejs.post-upgrade b/main/nodejs/nodejs.post-upgrade
new file mode 100644
index 000000000000..764aa17ff342
--- /dev/null
+++ b/main/nodejs/nodejs.post-upgrade
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+# This file is not provided since splitting npm into a separate aport,
+# so we use it to quickly detect presence of the old npm package.
+if [ -f /usr/lib/node_modules/npm/configure ]; then
+	pkg_ver=$(apk info -W /usr/bin/npm 2>/dev/null \
+	        | sed -En 's/.*owned by npm-([^-]+).*/\1/p' \
+		| grep .) || exit 0
+
+	npm_ver=$(/usr/bin/npm --version 2>/dev/null) || exit 0
+
+	[ "$pkg_ver" = "$npm_ver" ] && exit 0
+
+	cat >&2 <<-EOF
+	*
+	* You have an old version of the 'npm' package installed
+	* (pkg version: $pkg_ver, real version: $npm_ver). The newer package
+	* has a *lower* version number that now corresponds to the actual
+	* version of the 'npm' program. You have to reinstall the npm package
+	* (apk del npm; apk add npm) or upgrade all packages to the available
+	* versions (apk upgrade -a).
+	*
+	EOF
+fi
+
+exit 0
diff --git a/main/npm/APKBUILD b/main/npm/APKBUILD
new file mode 100644
index 000000000000..886a1e8a1c69
--- /dev/null
+++ b/main/npm/APKBUILD
@@ -0,0 +1,116 @@
+# Contributor: Jakub Jirutka <jakub@jirutka.cz>
+# Maintainer: Jakub Jirutka <jakub@jirutka.cz>
+pkgname=npm
+pkgver=7.9.0
+pkgrel=0
+pkgdesc="The package manager for JavaScript"
+url="https://npm.community"
+arch="noarch"
+license="Artistic-2.0"
+depends="nodejs"
+makedepends="bash"
+replaces="nodejs-doc"  # for backward compatibility
+subpackages="$pkgname-doc $pkgname-bashcomp"
+source="https://github.com/npm/cli/archive/v$pkgver/npm-cli-$pkgver.tar.gz
+	reproducible-documentation-build.patch
+	dont-check-for-last-version.patch
+	make-dont-install-deps.patch
+	smoke-tests-npm-location.patch
+	npmrc
+	"
+builddir="$srcdir/cli-$pkgver"
+
+prepare() {
+	default_prepare
+
+	# Remove bunch of unnecessary files to reduce size of the package.
+
+	rm bin/npm bin/npx bin/*.cmd bin/node-gyp-bin/*.cmd
+
+	cd node_modules
+	find . -type f \( \
+		-name '.*' -o \
+		-name '*.cmd' -o \
+		-name '*.coffee' -o \
+		-name '*.bat' -o \
+		-name '*.map' -o \
+		-name '*.md' -o \
+		-name '*.ts' -o \
+		-name 'AUTHORS*' -o \
+		-name 'LICENSE*' -o \
+		-name 'Makefile' -o \
+		-name 'README*' -o \) -delete
+	rm -Rf ./*/.git* ./*/doc ./*/docs ./*/examples ./*/scripts ./*/test
+
+	# No files should be executable here, except node-gyp.
+	find . -type f -executable ! -name 'node-gyp*' -exec chmod -x {} \;
+
+	cd ../docs/content
+
+	# XXX: Workaround for https://github.com/npm/cli/issues/780.
+	local f name
+	for f in configuring-npm/folders.md configuring-npm/install.md using-npm/*.md; do
+		name=$(basename $f .md)
+		sed -Ei "s/^title: $name/title: npm-$name/" "$f"
+		mv "$f" "$(dirname $f)/npm-$name.md"
+	done
+
+	cd "$builddir"
+
+	# Backup files that will be included in the package before installing
+	# additional dev dependencies and running tests.
+	mkdir -p "$srcdir"/npm
+	cp -r bin lib node_modules package.json "$srcdir"/npm/
+
+	# Install dependencies needed for mandocs and smoke-tests.
+	node bin/npm-cli.js install --ignore-scripts --no-audit --no-fund
+}
+
+build() {
+	# Convert Markdown docs to man pages.
+	make mandocs
+
+	# Generate bash completions.
+	node bin/npm-cli.js completion > npm.bash
+}
+
+check() {
+	make smoke-tests NPM_LOCATION="$srcdir/npm"
+}
+
+package() {
+	local destdir="$pkgdir/usr/lib/node_modules/npm"
+
+	mkdir -p "$destdir"
+	cp -r "$srcdir"/npm/* "$destdir"/
+
+	install -m 644 "$srcdir"/npmrc -t "$destdir"/
+
+	mkdir -p "$pkgdir"/usr/bin
+	ln -s ../lib/node_modules/npm/bin/npm-cli.js "$pkgdir"/usr/bin/npm
+	ln -s ../lib/node_modules/npm/bin/npx-cli.js "$pkgdir"/usr/bin/npx
+	ln -s ../lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js "$pkgdir"/usr/bin/node-gyp
+
+	cd "$builddir"
+
+	mkdir -p "$pkgdir"/usr/share
+	cp -r man "$pkgdir"/usr/share/
+	ln -s ../../../share/man "$destdir"/man
+
+	install -Dm 644 LICENSE -t "$pkgdir"/usr/share/licenses/$pkgname/
+
+	install -Dm 644 npm.bash "$pkgdir"/usr/share/bash-completion/completions/npm
+}
+
+doc() {
+	default_doc
+
+	amove usr/lib/node_modules/npm/man
+}
+
+sha512sums="ad59518cfa8990d61d662de6385489fa0f490f556d5de479890363feb74d8eadc3074e41c5899d09c94c15bc7661766eee20666fca97fac4178e6b26b9c7c6f7  npm-cli-7.9.0.tar.gz
+44821978050194c126a9d3c0fb9df9204100d8e88c13a9aa8c0e74769f3a667ce605c1f37e053a658dde6ee8367cf71005ea3f1705b3a95700767bc041a28f15  reproducible-documentation-build.patch
+92728357d9d03a40b275ba01980a569dc456f526a415edc22a4582162ff8dcafe8f32cb42f14bc93ea627f1ac9384a42abe2bcdfb8ed04c06491a27b1efc7e40  dont-check-for-last-version.patch
+0aed63f6b541378d51794393bf70b6401c7bdbc8c1ebbf35fc8b91c8220fc5ab1753f577908b0dc24bdeaf4f9cabb3bb1a48329c2ac026aba1f479d6a53311d6  make-dont-install-deps.patch
+bb0f40db679c4811531e0373cb133dc116cf0efe3e9b4619a73c6897ab440dd99c592fa4f682b72aaef311a231f5b02934b90f9aa538aadb106b49cb3f2ddd91  smoke-tests-npm-location.patch
+60c2077177503af872c77fe0fedb039596b40142cead5dc9bcc84675b217b0ce332d5d8da9c2483a6e995e84934290934d64bb216dcdfef7e24ab8e7ea1722c5  npmrc"
diff --git a/main/npm/dont-check-for-last-version.patch b/main/npm/dont-check-for-last-version.patch
new file mode 100644
index 000000000000..56ca23511aa9
--- /dev/null
+++ b/main/npm/dont-check-for-last-version.patch
@@ -0,0 +1,18 @@
+Description: don't check for last version
+Author: Xavier Guimard <yadd@debian.org>
+Forwarded: not-needed
+Last-Update: 2021-03-02
+
+Patch-Source: https://sources.debian.org/src/npm/7.5.2+ds-2/debian/patches/dont-check-for-last-version.patch
+
+--- a/lib/utils/update-notifier.js
++++ b/lib/utils/update-notifier.js
+@@ -38,6 +38,8 @@
+ }
+ 
+ const updateNotifier = module.exports = async (npm, spec = 'latest') => {
++  // Maintained by Debian JS Team
++  return null;
+   // never check for updates in CI, when updating npm already, or opted out
+   if (!npm.config.get('update-notifier') ||
+       isGlobalNpmUpdate(npm) ||
diff --git a/main/npm/make-dont-install-deps.patch b/main/npm/make-dont-install-deps.patch
new file mode 100644
index 000000000000..ac6aa0aa4971
--- /dev/null
+++ b/main/npm/make-dont-install-deps.patch
@@ -0,0 +1,11 @@
+--- a/Makefile
++++ b/Makefile
+@@ -47,7 +47,7 @@
+ 
+ ## build-time dependencies for the documentation
+ dev-deps:
+-	node bin/npm-cli.js install --no-audit --ignore-scripts
++	true  # skip
+ 
+ ## targets for man files, these are encouraged to be only built by running `make docs` or `make mandocs`
+ man/man1/%.1: docs/content/commands/%.md scripts/docs-build.js
diff --git a/main/npm/npmrc b/main/npm/npmrc
new file mode 100644
index 000000000000..c8a17e066e49
--- /dev/null
+++ b/main/npm/npmrc
@@ -0,0 +1,6 @@
+# Do not modify this file - use /etc/npmrc instead!
+
+globalconfig=/etc/npmrc
+globalignorefile=/etc/npmignore
+prefix=/usr
+python=/usr/bin/python3
diff --git a/main/npm/reproducible-documentation-build.patch b/main/npm/reproducible-documentation-build.patch
new file mode 100644
index 000000000000..2a02d418c7de
--- /dev/null
+++ b/main/npm/reproducible-documentation-build.patch
@@ -0,0 +1,20 @@
+Description: Use source-date-epoch as timestamp source for documentation
+Author: James Addison <jay+salsa@jp-hosting.net>
+Origin: https://salsa.debian.org/js-team/npm/-/merge_requests/7
+Forwarded: no
+Last-Update: 2020-11-11
+
+Patch-Source: https://sources.debian.org/src/npm/7.5.2+ds-2/debian/patches/2020_reproducible_documentation_build.patch
+
+--- a/scripts/docs-build.js
++++ b/scripts/docs-build.js
+@@ -34,7 +34,8 @@
+     .replace(/\[([^\]]+)\]\(\/using-npm\/([^)]+)\)/g, replacer)
+     .trim()
+ 
+-  fs.writeFile(dest, marked(result), 'utf8', function (err) {
++  var date = new Date(process.env.SOURCE_DATE_EPOCH)
++  fs.writeFile(dest, marked(result, {date}), 'utf8', function (err) {
+     if (err) return console.log(err)
+   })
+ })
diff --git a/main/npm/smoke-tests-npm-location.patch b/main/npm/smoke-tests-npm-location.patch
new file mode 100644
index 000000000000..79f96758faee
--- /dev/null
+++ b/main/npm/smoke-tests-npm-location.patch
@@ -0,0 +1,23 @@
+Allow to specify custom npm location for smoke-tests, so we can test npm
+without development dependencies involved.
+
+--- a/smoke-tests/index.js
++++ b/smoke-tests/index.js
+@@ -29,7 +29,7 @@
+ })
+ const localPrefix = resolve(path, 'project')
+ const userconfigLocation = resolve(path, '.npmrc')
+-const npmLocation = resolve(__dirname, '..')
++const npmLocation = resolve(__dirname, process.env.NPM_LOCATION || '..')
+ const cacheLocation = resolve(path, 'cache')
+ const binLocation = resolve(path, 'bin')
+ const env = {
+@@ -57,7 +57,7 @@
+   t.equal(pkg.version, '1.0.0', 'should have expected generated version')
+ })
+ 
+-t.test('npm (no args)', async t => {
++t.skip('npm (no args)', async t => {
+   const cmd = `"${process.execPath}" "${npmLocation}" --no-audit --no-update-notifier`
+   const cmdRes = await execAsync(cmd, { cwd: localPrefix, env })
+     .catch(err => {
-- 
GitLab