From: Stefan Herbrechtsmeier <stefan.herbrechtsme...@weidmueller.com>

Rework the npm class to use plain npm commands and remove the usage of
the npm cache to speed-up builds.

Signed-off-by: Stefan Herbrechtsmeier <stefan.herbrechtsme...@weidmueller.com>
---

 meta/classes/npm.bbclass        | 340 +++++++++++---------------------
 scripts/lib/devtool/standard.py |   6 +-
 2 files changed, 119 insertions(+), 227 deletions(-)

diff --git a/meta/classes/npm.bbclass b/meta/classes/npm.bbclass
index ba50fcac20..cb22dc6998 100644
--- a/meta/classes/npm.bbclass
+++ b/meta/classes/npm.bbclass
@@ -1,7 +1,12 @@
 # Copyright (C) 2020 Savoir-Faire Linux
 #
+# Copyright (C) 2021 Weidmüller Interface GmbH & Co. KG
+# Author: Stefan Herbrechtsmeier <stefan.herbrechtsme...@weidmueller.com>
+#
 # SPDX-License-Identifier: GPL-2.0-only
 #
+# A bbclass to install an npm package and build its dependencies.
+#
 # This bbclass builds and installs an npm package to the target. The package
 # sources files should be fetched in the calling recipe by using the SRC_URI
 # variable. The ${S} variable should be updated depending of your fetcher.
@@ -16,18 +21,29 @@
 #
 #  NPM_INSTALL_DEV:
 #       Set to 1 to also install devDependencies.
+#
+#  NPM_BUILD_PRUNE_FOR_PRODUCTION
+#       Set to 0 to keep installed devDependencies.
 
 inherit python3native
 
 DEPENDS:prepend = "nodejs-native "
-RDEPENDS:${PN}:append:class-target = " nodejs"
+NPM_RDEPENDS = "nodejs"
+RDEPENDS:${PN}:append:class-target = " ${NPM_RDEPENDS}"
 
-EXTRA_OENPM = ""
+EXTRA_OENPM ?= ""
+EXTRA_OENPM_BUILD ?= ""
 
 NPM_INSTALL_DEV ?= "0"
 
 NPM_NODEDIR ?= "${RECIPE_SYSROOT_NATIVE}${prefix_native}"
 
+def npm_flag_dev(value):
+    if bb.utils.to_boolean(value, False):
+        return "--also=development"
+    else:
+        return "--only=production"
+
 def npm_target_arch_map(target_arch):
     """Maps arch names to npm arch names"""
     import re
@@ -43,247 +59,124 @@ def npm_target_arch_map(target_arch):
 
 NPM_ARCH ?= "${@npm_target_arch_map(d.getVar("TARGET_ARCH"))}"
 
-NPM_PACKAGE = "${WORKDIR}/npm-package"
 NPM_CACHE = "${WORKDIR}/npm-cache"
-NPM_BUILD = "${WORKDIR}/npm-build"
-
-def npm_global_configs(d):
-    """Get the npm global configuration"""
-    configs = []
-    # Ensure no network access is done
-    configs.append(("offline", "true"))
-    configs.append(("proxy", "http://invalid";))
-    # Configure the cache directory
-    configs.append(("cache", d.getVar("NPM_CACHE")))
-    return configs
-
-def npm_pack(env, srcdir, workdir):
-    """Run 'npm pack' on a specified directory"""
-    import shlex
-    cmd = "npm pack %s" % shlex.quote(srcdir)
-    args = [("ignore-scripts", "true")]
-    tarball = env.run(cmd, args=args, workdir=workdir).strip("\n")
-    return os.path.join(workdir, tarball)
-
-python npm_do_configure() {
-    """
-    Step one: configure the npm cache and the main npm package
-
-    Every dependencies have been fetched and patched in the source directory.
-    They have to be packed (this remove unneeded files) and added to the npm
-    cache to be available for the next step.
-
-    The main package and its associated manifest file and shrinkwrap file have
-    to be configured to take into account these cached dependencies.
-    """
-    import base64
-    import copy
-    import json
-    import re
-    import shlex
-    import tempfile
-    from bb.fetch2.npm import NpmEnvironment
-    from bb.fetch2.npm import npm_unpack
-    from bb.fetch2.npmsw import foreach_dependencies
-    from bb.progress import OutOfProgressHandler
-
-    bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
-    bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
-
-    env = NpmEnvironment(d, configs=npm_global_configs(d))
-
-    def _npm_cache_add(tarball):
-        """Run 'npm cache add' for a specified tarball"""
-        cmd = "npm cache add %s" % shlex.quote(tarball)
-        env.run(cmd)
-
-    def _npm_integrity(tarball):
-        """Return the npm integrity of a specified tarball"""
-        sha512 = bb.utils.sha512_file(tarball)
-        return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
-
-    def _npm_version(tarball):
-        """Return the version of a specified tarball"""
-        regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
-        return re.search(regex, tarball).group(1)
-
-    def _npmsw_dependency_dict(orig, deptree):
-        """
-        Return the sub dictionary in the 'orig' dictionary corresponding to the
-        'deptree' dependency tree. This function follows the shrinkwrap file
-        format.
-        """
-        ptr = orig
-        for dep in deptree:
-            if "dependencies" not in ptr:
-                ptr["dependencies"] = {}
-            ptr = ptr["dependencies"]
-            if dep not in ptr:
-                ptr[dep] = {}
-            ptr = ptr[dep]
-        return ptr
-
-    # Manage the manifest file and shrinkwrap files
-    orig_manifest_file = d.expand("${S}/package.json")
-    orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
-    cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
-    cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
-
-    with open(orig_manifest_file, "r") as f:
-        orig_manifest = json.load(f)
-
-    cached_manifest = copy.deepcopy(orig_manifest)
-    cached_manifest.pop("dependencies", None)
-    cached_manifest.pop("devDependencies", None)
-
-    has_shrinkwrap_file = True
-
-    try:
-        with open(orig_shrinkwrap_file, "r") as f:
-            orig_shrinkwrap = json.load(f)
-    except IOError:
-        has_shrinkwrap_file = False
-
-    if has_shrinkwrap_file:
-       cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
-       cached_shrinkwrap.pop("dependencies", None)
-
-    # Manage the dependencies
-    progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
-    progress_total = 1 # also count the main package
-    progress_done = 0
-
-    def _count_dependency(name, params, deptree):
-        nonlocal progress_total
-        progress_total += 1
-
-    def _cache_dependency(name, params, deptree):
-        destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
-        destsuffix = os.path.join(*destsubdirs)
-        with tempfile.TemporaryDirectory() as tmpdir:
-            # Add the dependency to the npm cache
-            destdir = os.path.join(d.getVar("S"), destsuffix)
-            tarball = npm_pack(env, destdir, tmpdir)
-            _npm_cache_add(tarball)
-            # Add its signature to the cached shrinkwrap
-            dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
-            dep["version"] = _npm_version(tarball)
-            dep["integrity"] = _npm_integrity(tarball)
-            if params.get("dev", False):
-                dep["dev"] = True
-            # Display progress
-            nonlocal progress_done
-            progress_done += 1
-            progress.write("%d/%d" % (progress_done, progress_total))
-
-    dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
-
-    if has_shrinkwrap_file:
-        foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
-        foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
-
-    # Configure the main package
-    with tempfile.TemporaryDirectory() as tmpdir:
-        tarball = npm_pack(env, d.getVar("S"), tmpdir)
-        npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
-
-    # Configure the cached manifest file and cached shrinkwrap file
-    def _update_manifest(depkey):
-        for name in orig_manifest.get(depkey, {}):
-            version = cached_shrinkwrap["dependencies"][name]["version"]
-            if depkey not in cached_manifest:
-                cached_manifest[depkey] = {}
-            cached_manifest[depkey][name] = version
-
-    if has_shrinkwrap_file:
-        _update_manifest("dependencies")
-
-    if dev:
-        if has_shrinkwrap_file:
-            _update_manifest("devDependencies")
-
-    with open(cached_manifest_file, "w") as f:
-        json.dump(cached_manifest, f, indent=2)
-
-    if has_shrinkwrap_file:
-        with open(cached_shrinkwrap_file, "w") as f:
-            json.dump(cached_shrinkwrap, f, indent=2)
-}
-
-python npm_do_compile() {
-    """
-    Step two: install the npm package
 
-    Use the configured main package and the cached dependencies to run the
-    installation process. The installation is done in a directory which is
-    not the destination directory yet.
+NPM_PRUNE_FOR_PRODUCTION ?= "1"
 
-    A combination of 'npm pack' and 'npm install' is used to ensure that the
-    installed files are actual copies instead of symbolic links (which is the
-    default npm behavior).
-    """
-    import shlex
-    import tempfile
-    from bb.fetch2.npm import NpmEnvironment
+B = "${WORKDIR}/build"
 
-    bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
+NPM_SOURCEPATH ?= "${S}"
 
-    with tempfile.TemporaryDirectory() as tmpdir:
-        args = []
-        configs = npm_global_configs(d)
+export NPM_CONFIG_GLOBALCONFIG = "${WORKDIR}/npmrc"
+export NPM_CONFIG_USERCONFIG = "/dev/null"
 
-        if bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False):
-            configs.append(("also", "development"))
-        else:
-            configs.append(("only", "production"))
-
-        # Report as many logs as possible for debugging purpose
-        configs.append(("loglevel", "silly"))
+oe_runnpm_plain() {
+    HOME=${B}/root npm "$@"
+}
 
-        # Configure the installation to be done globally in the build directory
-        configs.append(("global", "true"))
-        configs.append(("prefix", d.getVar("NPM_BUILD")))
+oe_runnpm() {
+    bbnote npm "$@"
+    oe_runnpm_plain "$@"
+}
 
-        # Add node-gyp configuration
-        configs.append(("arch", d.getVar("NPM_ARCH")))
-        configs.append(("release", "true"))
-        configs.append(("nodedir", d.getVar("NPM_NODEDIR")))
-        configs.append(("python", d.getVar("PYTHON")))
+oe_runnpm_pack() {
+    oe_runnpm_plain pack --ignore-scripts --loglevel=warn "$@"
+}
 
-        env = NpmEnvironment(d, configs)
+oe_runnpm_rebuild() {
+    arch=$1
+    shift
 
-        # Add node-pre-gyp configuration
-        args.append(("target_arch", d.getVar("NPM_ARCH")))
-        args.append(("build-from-source", "true"))
+    # Create symlinks for package executables
+    # because rebuild doesn't respect the dependency tree
+    oe_runnpm rebuild --ignore-scripts
 
-        # Pack and install the main package
-        tarball = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir)
-        cmd = "npm install %s %s" % (shlex.quote(tarball), 
d.getVar("EXTRA_OENPM"))
-        env.run(cmd, args=args)
+    # Rebuild dependencies
+    oe_runnpm rebuild \
+        --arch=${arch} \
+        --target_arch=${arch} \
+        --build-from-source \
+        "$@"
 }
 
-npm_do_install() {
-    # Step three: final install
-    #
-    # The previous installation have to be filtered to remove some extra files.
+do_generate_npmrc() {
+    cat > ${NPM_CONFIG_GLOBALCONFIG} <<EOF
+cache=${NPM_CACHE}
+loglevel=silly
+offline=true
+package-lock=false
+proxy=http://invalid
+; node-gyp
+arch=${NPM_ARCH}
+nodedir=${NPM_NODEDIR}
+python=${PYTHON}
+release=true
+EOF
+}
+do_generate_npmrc[vardeps] += "NPM_ARCH NPM_CACHE NPM_CONFIG_GLOBALCONFIG 
NPM_NODEDIR PYTHON"
+addtask generate_npmrc after do_patch before do_populate_cache
 
-    rm -rf ${D}
+npm_do_configure() {
+    :
+}
 
-    # Copy the entire lib and bin directories
-    install -d ${D}/${nonarch_libdir}
-    cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. 
${D}/${nonarch_libdir}
+npm_do_compile() {
+    # Create a tarball from main npm package
+    # to respect ignore files and files property of package.json
+    cd ${B}/pkg
+    tarball=$(oe_runnpm_pack ${NPM_SOURCEPATH})
+    [ -n "${tarball}" ] || exit 1
+
+    # Unpack main npm package
+    mkdir ${B}/lib/node_modules/${BPN}/
+    tar -xzf ${B}/pkg/$tarball --no-same-owner \
+        --strip-components=1 -C ${B}/lib/node_modules/${BPN}
+
+    # Copy npm dependencies from sources
+    if [ -d ${S}/node_modules ]; then
+        # Copy sources without preserve mode and ownership
+        # to prevent ugly file permissions
+        mkdir ${B}/lib/node_modules/${BPN}/node_modules
+        cp -a --no-preserve=mode,ownership \
+            ${S}/node_modules/. ${B}/lib/node_modules/${BPN}/node_modules/
+
+        # Prune dependencies for production
+        if [ "${NPM_INSTALL_DEV}" = "1" -a "${NPM_PRUNE_FOR_PRODUCTION}" = "1" 
] ; then
+            oe_runnpm prune --only=production
+        fi
+
+        # Rebuild dependencies inside node_modules folder
+        # to create bin links in .bin folder
+        cd ${B}/lib/node_modules
+        oe_runnpm_rebuild ${NPM_ARCH} ${EXTRA_OENPM}
+    fi
+}
+do_compile[cleandirs] = "${B}/lib/node_modules ${B}/pkg"
 
-    if [ -d "${NPM_BUILD}/bin" ]
-    then
-        install -d ${D}/${bindir}
-        cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. 
${D}/${bindir}
+npm_do_install() {
+    # Copy the entire main node_modules directory
+    install -d ${D}${nonarch_libdir}
+    cp -a --no-preserve=ownership ${B}/lib/node_modules \
+        ${D}${nonarch_libdir}/
+
+    # Create symlinks for package executables
+    if [ -d ${D}${nonarch_libdir}/node_modules/.bin ]; then
+        install -d ${D}${bindir}
+        for f in ${D}${nonarch_libdir}/node_modules/.bin/*; do
+            name="$(basename $f)"
+            link="$(readlink $f)"
+            target="$(realpath -m --relative-to ${D}${bindir} \
+                ${D}${nonarch_libdir}/node_modules/.bin/$link)"
+            ln -s "$target" "${D}${bindir}/$name"
+        done
+        rm -rf ${D}${nonarch_libdir}/node_modules/.bin
     fi
 
     # If the package (or its dependencies) uses node-gyp to build native 
addons,
     # object files, static libraries or other temporary files can be hidden in
     # the lib directory. To reduce the package size and to avoid QA issues
     # (staticdev with static library files) these files must be removed.
-    local GYP_REGEX=".*/build/Release/[^/]*.node"
+    local GYP_REGEX=".*/build.*/Release/[^/]*.node"
 
     # Remove any node-gyp directory in ${D} to remove temporary build files
     for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
@@ -294,9 +187,9 @@ npm_do_install() {
     done
 
     # Copy only the node-gyp release files
-    for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
+    for GYP_B_FILE in $(find ${B}/pkg -regex "${GYP_REGEX}")
     do
-        local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
+        local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${B}/pkg}
 
         install -d ${GYP_D_FILE%/*}
         install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
@@ -310,6 +203,7 @@ npm_do_install() {
     # using /usr/lib/node_modules as install directory. Let's make both happy.
     ln -fs node_modules ${D}/${nonarch_libdir}/node
 }
+do_install[cleandirs] = "${D}"
 
 FILES:${PN} += " \
     ${bindir} \
diff --git a/scripts/lib/devtool/standard.py b/scripts/lib/devtool/standard.py
index 01fb5ad96f..10dc2b1373 100644
--- a/scripts/lib/devtool/standard.py
+++ b/scripts/lib/devtool/standard.py
@@ -260,10 +260,8 @@ def add(args, config, basepath, workspace):
                 f.write('}\n')
 
             if bb.data.inherits_class('npm', rd):
-                f.write('python do_configure:append() {\n')
-                f.write('    pkgdir = d.getVar("NPM_PACKAGE")\n')
-                f.write('    lockfile = os.path.join(pkgdir, 
"singletask.lock")\n')
-                f.write('    bb.utils.remove(lockfile)\n')
+                f.write('do_configure:append() {\n')
+                f.write('    rm -f ${NPM_PACKAGE}/singletask.lock\n')
                 f.write('}\n')
 
         # Check if the new layer provides recipes whose priorities have been
-- 
2.20.1

-=-=-=-=-=-=-=-=-=-=-=-
Links: You receive all messages sent to this group.
View/Reply Online (#158697): 
https://lists.openembedded.org/g/openembedded-core/message/158697
Mute This Topic: https://lists.openembedded.org/mt/87282275/21656
Group Owner: openembedded-core+ow...@lists.openembedded.org
Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub 
[arch...@mail-archive.com]
-=-=-=-=-=-=-=-=-=-=-=-

Reply via email to