1*4882a593Smuzhiyun# Copyright (C) 2020 Savoir-Faire Linux 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun# This bbclass builds and installs an npm package to the target. The package 6*4882a593Smuzhiyun# sources files should be fetched in the calling recipe by using the SRC_URI 7*4882a593Smuzhiyun# variable. The ${S} variable should be updated depending of your fetcher. 8*4882a593Smuzhiyun# 9*4882a593Smuzhiyun# Usage: 10*4882a593Smuzhiyun# SRC_URI = "..." 11*4882a593Smuzhiyun# inherit npm 12*4882a593Smuzhiyun# 13*4882a593Smuzhiyun# Optional variables: 14*4882a593Smuzhiyun# NPM_ARCH: 15*4882a593Smuzhiyun# Override the auto generated npm architecture. 16*4882a593Smuzhiyun# 17*4882a593Smuzhiyun# NPM_INSTALL_DEV: 18*4882a593Smuzhiyun# Set to 1 to also install devDependencies. 19*4882a593Smuzhiyun 20*4882a593Smuzhiyuninherit python3native 21*4882a593Smuzhiyun 22*4882a593SmuzhiyunDEPENDS:prepend = "nodejs-native nodejs-oe-cache-native " 23*4882a593SmuzhiyunRDEPENDS:${PN}:append:class-target = " nodejs" 24*4882a593Smuzhiyun 25*4882a593SmuzhiyunEXTRA_OENPM = "" 26*4882a593Smuzhiyun 27*4882a593SmuzhiyunNPM_INSTALL_DEV ?= "0" 28*4882a593Smuzhiyun 29*4882a593SmuzhiyunNPM_NODEDIR ?= "${RECIPE_SYSROOT_NATIVE}${prefix_native}" 30*4882a593Smuzhiyun 31*4882a593Smuzhiyundef npm_target_arch_map(target_arch): 32*4882a593Smuzhiyun """Maps arch names to npm arch names""" 33*4882a593Smuzhiyun import re 34*4882a593Smuzhiyun if re.match("p(pc|owerpc)(|64)", target_arch): 35*4882a593Smuzhiyun return "ppc" 36*4882a593Smuzhiyun elif re.match("i.86$", target_arch): 37*4882a593Smuzhiyun return "ia32" 38*4882a593Smuzhiyun elif re.match("x86_64$", target_arch): 39*4882a593Smuzhiyun return "x64" 40*4882a593Smuzhiyun elif re.match("arm64$", target_arch): 41*4882a593Smuzhiyun return "arm" 42*4882a593Smuzhiyun return target_arch 43*4882a593Smuzhiyun 44*4882a593SmuzhiyunNPM_ARCH ?= "${@npm_target_arch_map(d.getVar("TARGET_ARCH"))}" 45*4882a593Smuzhiyun 46*4882a593SmuzhiyunNPM_PACKAGE = "${WORKDIR}/npm-package" 47*4882a593SmuzhiyunNPM_CACHE = "${WORKDIR}/npm-cache" 48*4882a593SmuzhiyunNPM_BUILD = "${WORKDIR}/npm-build" 49*4882a593SmuzhiyunNPM_REGISTRY = "${WORKDIR}/npm-registry" 50*4882a593Smuzhiyun 51*4882a593Smuzhiyundef npm_global_configs(d): 52*4882a593Smuzhiyun """Get the npm global configuration""" 53*4882a593Smuzhiyun configs = [] 54*4882a593Smuzhiyun # Ensure no network access is done 55*4882a593Smuzhiyun configs.append(("offline", "true")) 56*4882a593Smuzhiyun configs.append(("proxy", "http://invalid")) 57*4882a593Smuzhiyun # Configure the cache directory 58*4882a593Smuzhiyun configs.append(("cache", d.getVar("NPM_CACHE"))) 59*4882a593Smuzhiyun return configs 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun## 'npm pack' runs 'prepare' and 'prepack' scripts. Support for 62*4882a593Smuzhiyun## 'ignore-scripts' which prevents this behavior has been removed 63*4882a593Smuzhiyun## from nodejs 16. Use simple 'tar' instead of. 64*4882a593Smuzhiyundef npm_pack(env, srcdir, workdir): 65*4882a593Smuzhiyun """Emulate 'npm pack' on a specified directory""" 66*4882a593Smuzhiyun import subprocess 67*4882a593Smuzhiyun import os 68*4882a593Smuzhiyun import json 69*4882a593Smuzhiyun 70*4882a593Smuzhiyun src = os.path.join(srcdir, 'package.json') 71*4882a593Smuzhiyun with open(src) as f: 72*4882a593Smuzhiyun j = json.load(f) 73*4882a593Smuzhiyun 74*4882a593Smuzhiyun # base does not really matter and is for documentation purposes 75*4882a593Smuzhiyun # only. But the 'version' part must exist because other parts of 76*4882a593Smuzhiyun # the bbclass rely on it. 77*4882a593Smuzhiyun base = j['name'].split('/')[-1] 78*4882a593Smuzhiyun tarball = os.path.join(workdir, "%s-%s.tgz" % (base, j['version'])); 79*4882a593Smuzhiyun 80*4882a593Smuzhiyun # TODO: real 'npm pack' does not include directories while 'tar' 81*4882a593Smuzhiyun # does. But this does not seem to matter... 82*4882a593Smuzhiyun subprocess.run(['tar', 'czf', tarball, 83*4882a593Smuzhiyun '--exclude', './node-modules', 84*4882a593Smuzhiyun '--exclude-vcs', 85*4882a593Smuzhiyun '--transform', 's,^\./,package/,', 86*4882a593Smuzhiyun '--mtime', '1985-10-26T08:15:00.000Z', 87*4882a593Smuzhiyun '.'], 88*4882a593Smuzhiyun check = True, cwd = srcdir) 89*4882a593Smuzhiyun 90*4882a593Smuzhiyun return (tarball, j) 91*4882a593Smuzhiyun 92*4882a593Smuzhiyunpython npm_do_configure() { 93*4882a593Smuzhiyun """ 94*4882a593Smuzhiyun Step one: configure the npm cache and the main npm package 95*4882a593Smuzhiyun 96*4882a593Smuzhiyun Every dependencies have been fetched and patched in the source directory. 97*4882a593Smuzhiyun They have to be packed (this remove unneeded files) and added to the npm 98*4882a593Smuzhiyun cache to be available for the next step. 99*4882a593Smuzhiyun 100*4882a593Smuzhiyun The main package and its associated manifest file and shrinkwrap file have 101*4882a593Smuzhiyun to be configured to take into account these cached dependencies. 102*4882a593Smuzhiyun """ 103*4882a593Smuzhiyun import base64 104*4882a593Smuzhiyun import copy 105*4882a593Smuzhiyun import json 106*4882a593Smuzhiyun import re 107*4882a593Smuzhiyun import shlex 108*4882a593Smuzhiyun import tempfile 109*4882a593Smuzhiyun from bb.fetch2.npm import NpmEnvironment 110*4882a593Smuzhiyun from bb.fetch2.npm import npm_unpack 111*4882a593Smuzhiyun from bb.fetch2.npmsw import foreach_dependencies 112*4882a593Smuzhiyun from bb.progress import OutOfProgressHandler 113*4882a593Smuzhiyun from oe.npm_registry import NpmRegistry 114*4882a593Smuzhiyun 115*4882a593Smuzhiyun bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True) 116*4882a593Smuzhiyun bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True) 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun env = NpmEnvironment(d, configs=npm_global_configs(d)) 119*4882a593Smuzhiyun registry = NpmRegistry(d.getVar('NPM_REGISTRY'), d.getVar('NPM_CACHE')) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun def _npm_cache_add(tarball, pkg): 122*4882a593Smuzhiyun """Add tarball to local registry and register it in the 123*4882a593Smuzhiyun cache""" 124*4882a593Smuzhiyun registry.add_pkg(tarball, pkg) 125*4882a593Smuzhiyun 126*4882a593Smuzhiyun def _npm_integrity(tarball): 127*4882a593Smuzhiyun """Return the npm integrity of a specified tarball""" 128*4882a593Smuzhiyun sha512 = bb.utils.sha512_file(tarball) 129*4882a593Smuzhiyun return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode() 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun def _npmsw_dependency_dict(orig, deptree): 132*4882a593Smuzhiyun """ 133*4882a593Smuzhiyun Return the sub dictionary in the 'orig' dictionary corresponding to the 134*4882a593Smuzhiyun 'deptree' dependency tree. This function follows the shrinkwrap file 135*4882a593Smuzhiyun format. 136*4882a593Smuzhiyun """ 137*4882a593Smuzhiyun ptr = orig 138*4882a593Smuzhiyun for dep in deptree: 139*4882a593Smuzhiyun if "dependencies" not in ptr: 140*4882a593Smuzhiyun ptr["dependencies"] = {} 141*4882a593Smuzhiyun ptr = ptr["dependencies"] 142*4882a593Smuzhiyun if dep not in ptr: 143*4882a593Smuzhiyun ptr[dep] = {} 144*4882a593Smuzhiyun ptr = ptr[dep] 145*4882a593Smuzhiyun return ptr 146*4882a593Smuzhiyun 147*4882a593Smuzhiyun # Manage the manifest file and shrinkwrap files 148*4882a593Smuzhiyun orig_manifest_file = d.expand("${S}/package.json") 149*4882a593Smuzhiyun orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json") 150*4882a593Smuzhiyun cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json") 151*4882a593Smuzhiyun cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json") 152*4882a593Smuzhiyun 153*4882a593Smuzhiyun with open(orig_manifest_file, "r") as f: 154*4882a593Smuzhiyun orig_manifest = json.load(f) 155*4882a593Smuzhiyun 156*4882a593Smuzhiyun cached_manifest = copy.deepcopy(orig_manifest) 157*4882a593Smuzhiyun cached_manifest.pop("dependencies", None) 158*4882a593Smuzhiyun cached_manifest.pop("devDependencies", None) 159*4882a593Smuzhiyun 160*4882a593Smuzhiyun has_shrinkwrap_file = True 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun try: 163*4882a593Smuzhiyun with open(orig_shrinkwrap_file, "r") as f: 164*4882a593Smuzhiyun orig_shrinkwrap = json.load(f) 165*4882a593Smuzhiyun except IOError: 166*4882a593Smuzhiyun has_shrinkwrap_file = False 167*4882a593Smuzhiyun 168*4882a593Smuzhiyun if has_shrinkwrap_file: 169*4882a593Smuzhiyun cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap) 170*4882a593Smuzhiyun cached_shrinkwrap.pop("dependencies", None) 171*4882a593Smuzhiyun 172*4882a593Smuzhiyun # Manage the dependencies 173*4882a593Smuzhiyun progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$") 174*4882a593Smuzhiyun progress_total = 1 # also count the main package 175*4882a593Smuzhiyun progress_done = 0 176*4882a593Smuzhiyun 177*4882a593Smuzhiyun def _count_dependency(name, params, deptree): 178*4882a593Smuzhiyun nonlocal progress_total 179*4882a593Smuzhiyun progress_total += 1 180*4882a593Smuzhiyun 181*4882a593Smuzhiyun def _cache_dependency(name, params, deptree): 182*4882a593Smuzhiyun destsubdirs = [os.path.join("node_modules", dep) for dep in deptree] 183*4882a593Smuzhiyun destsuffix = os.path.join(*destsubdirs) 184*4882a593Smuzhiyun with tempfile.TemporaryDirectory() as tmpdir: 185*4882a593Smuzhiyun # Add the dependency to the npm cache 186*4882a593Smuzhiyun destdir = os.path.join(d.getVar("S"), destsuffix) 187*4882a593Smuzhiyun (tarball, pkg) = npm_pack(env, destdir, tmpdir) 188*4882a593Smuzhiyun _npm_cache_add(tarball, pkg) 189*4882a593Smuzhiyun # Add its signature to the cached shrinkwrap 190*4882a593Smuzhiyun dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree) 191*4882a593Smuzhiyun dep["version"] = pkg['version'] 192*4882a593Smuzhiyun dep["integrity"] = _npm_integrity(tarball) 193*4882a593Smuzhiyun if params.get("dev", False): 194*4882a593Smuzhiyun dep["dev"] = True 195*4882a593Smuzhiyun # Display progress 196*4882a593Smuzhiyun nonlocal progress_done 197*4882a593Smuzhiyun progress_done += 1 198*4882a593Smuzhiyun progress.write("%d/%d" % (progress_done, progress_total)) 199*4882a593Smuzhiyun 200*4882a593Smuzhiyun dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False) 201*4882a593Smuzhiyun 202*4882a593Smuzhiyun if has_shrinkwrap_file: 203*4882a593Smuzhiyun foreach_dependencies(orig_shrinkwrap, _count_dependency, dev) 204*4882a593Smuzhiyun foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev) 205*4882a593Smuzhiyun 206*4882a593Smuzhiyun # Configure the main package 207*4882a593Smuzhiyun with tempfile.TemporaryDirectory() as tmpdir: 208*4882a593Smuzhiyun (tarball, _) = npm_pack(env, d.getVar("S"), tmpdir) 209*4882a593Smuzhiyun npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d) 210*4882a593Smuzhiyun 211*4882a593Smuzhiyun # Configure the cached manifest file and cached shrinkwrap file 212*4882a593Smuzhiyun def _update_manifest(depkey): 213*4882a593Smuzhiyun for name in orig_manifest.get(depkey, {}): 214*4882a593Smuzhiyun version = cached_shrinkwrap["dependencies"][name]["version"] 215*4882a593Smuzhiyun if depkey not in cached_manifest: 216*4882a593Smuzhiyun cached_manifest[depkey] = {} 217*4882a593Smuzhiyun cached_manifest[depkey][name] = version 218*4882a593Smuzhiyun 219*4882a593Smuzhiyun if has_shrinkwrap_file: 220*4882a593Smuzhiyun _update_manifest("dependencies") 221*4882a593Smuzhiyun 222*4882a593Smuzhiyun if dev: 223*4882a593Smuzhiyun if has_shrinkwrap_file: 224*4882a593Smuzhiyun _update_manifest("devDependencies") 225*4882a593Smuzhiyun 226*4882a593Smuzhiyun with open(cached_manifest_file, "w") as f: 227*4882a593Smuzhiyun json.dump(cached_manifest, f, indent=2) 228*4882a593Smuzhiyun 229*4882a593Smuzhiyun if has_shrinkwrap_file: 230*4882a593Smuzhiyun with open(cached_shrinkwrap_file, "w") as f: 231*4882a593Smuzhiyun json.dump(cached_shrinkwrap, f, indent=2) 232*4882a593Smuzhiyun} 233*4882a593Smuzhiyun 234*4882a593Smuzhiyunpython npm_do_compile() { 235*4882a593Smuzhiyun """ 236*4882a593Smuzhiyun Step two: install the npm package 237*4882a593Smuzhiyun 238*4882a593Smuzhiyun Use the configured main package and the cached dependencies to run the 239*4882a593Smuzhiyun installation process. The installation is done in a directory which is 240*4882a593Smuzhiyun not the destination directory yet. 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun A combination of 'npm pack' and 'npm install' is used to ensure that the 243*4882a593Smuzhiyun installed files are actual copies instead of symbolic links (which is the 244*4882a593Smuzhiyun default npm behavior). 245*4882a593Smuzhiyun """ 246*4882a593Smuzhiyun import shlex 247*4882a593Smuzhiyun import tempfile 248*4882a593Smuzhiyun from bb.fetch2.npm import NpmEnvironment 249*4882a593Smuzhiyun 250*4882a593Smuzhiyun bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True) 251*4882a593Smuzhiyun 252*4882a593Smuzhiyun with tempfile.TemporaryDirectory() as tmpdir: 253*4882a593Smuzhiyun args = [] 254*4882a593Smuzhiyun configs = npm_global_configs(d) 255*4882a593Smuzhiyun 256*4882a593Smuzhiyun if bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False): 257*4882a593Smuzhiyun configs.append(("also", "development")) 258*4882a593Smuzhiyun else: 259*4882a593Smuzhiyun configs.append(("only", "production")) 260*4882a593Smuzhiyun 261*4882a593Smuzhiyun # Report as many logs as possible for debugging purpose 262*4882a593Smuzhiyun configs.append(("loglevel", "silly")) 263*4882a593Smuzhiyun 264*4882a593Smuzhiyun # Configure the installation to be done globally in the build directory 265*4882a593Smuzhiyun configs.append(("global", "true")) 266*4882a593Smuzhiyun configs.append(("prefix", d.getVar("NPM_BUILD"))) 267*4882a593Smuzhiyun 268*4882a593Smuzhiyun # Add node-gyp configuration 269*4882a593Smuzhiyun configs.append(("arch", d.getVar("NPM_ARCH"))) 270*4882a593Smuzhiyun configs.append(("release", "true")) 271*4882a593Smuzhiyun configs.append(("nodedir", d.getVar("NPM_NODEDIR"))) 272*4882a593Smuzhiyun configs.append(("python", d.getVar("PYTHON"))) 273*4882a593Smuzhiyun 274*4882a593Smuzhiyun env = NpmEnvironment(d, configs) 275*4882a593Smuzhiyun 276*4882a593Smuzhiyun # Add node-pre-gyp configuration 277*4882a593Smuzhiyun args.append(("target_arch", d.getVar("NPM_ARCH"))) 278*4882a593Smuzhiyun args.append(("build-from-source", "true")) 279*4882a593Smuzhiyun 280*4882a593Smuzhiyun # Pack and install the main package 281*4882a593Smuzhiyun (tarball, _) = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir) 282*4882a593Smuzhiyun cmd = "npm install %s %s" % (shlex.quote(tarball), d.getVar("EXTRA_OENPM")) 283*4882a593Smuzhiyun env.run(cmd, args=args) 284*4882a593Smuzhiyun} 285*4882a593Smuzhiyun 286*4882a593Smuzhiyunnpm_do_install() { 287*4882a593Smuzhiyun # Step three: final install 288*4882a593Smuzhiyun # 289*4882a593Smuzhiyun # The previous installation have to be filtered to remove some extra files. 290*4882a593Smuzhiyun 291*4882a593Smuzhiyun rm -rf ${D} 292*4882a593Smuzhiyun 293*4882a593Smuzhiyun # Copy the entire lib and bin directories 294*4882a593Smuzhiyun install -d ${D}/${nonarch_libdir} 295*4882a593Smuzhiyun cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir} 296*4882a593Smuzhiyun 297*4882a593Smuzhiyun if [ -d "${NPM_BUILD}/bin" ] 298*4882a593Smuzhiyun then 299*4882a593Smuzhiyun install -d ${D}/${bindir} 300*4882a593Smuzhiyun cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir} 301*4882a593Smuzhiyun fi 302*4882a593Smuzhiyun 303*4882a593Smuzhiyun # If the package (or its dependencies) uses node-gyp to build native addons, 304*4882a593Smuzhiyun # object files, static libraries or other temporary files can be hidden in 305*4882a593Smuzhiyun # the lib directory. To reduce the package size and to avoid QA issues 306*4882a593Smuzhiyun # (staticdev with static library files) these files must be removed. 307*4882a593Smuzhiyun local GYP_REGEX=".*/build/Release/[^/]*.node" 308*4882a593Smuzhiyun 309*4882a593Smuzhiyun # Remove any node-gyp directory in ${D} to remove temporary build files 310*4882a593Smuzhiyun for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}") 311*4882a593Smuzhiyun do 312*4882a593Smuzhiyun local GYP_D_DIR=${GYP_D_FILE%/Release/*} 313*4882a593Smuzhiyun 314*4882a593Smuzhiyun rm --recursive --force ${GYP_D_DIR} 315*4882a593Smuzhiyun done 316*4882a593Smuzhiyun 317*4882a593Smuzhiyun # Copy only the node-gyp release files 318*4882a593Smuzhiyun for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}") 319*4882a593Smuzhiyun do 320*4882a593Smuzhiyun local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}} 321*4882a593Smuzhiyun 322*4882a593Smuzhiyun install -d ${GYP_D_FILE%/*} 323*4882a593Smuzhiyun install -m 755 ${GYP_B_FILE} ${GYP_D_FILE} 324*4882a593Smuzhiyun done 325*4882a593Smuzhiyun 326*4882a593Smuzhiyun # Remove the shrinkwrap file which does not need to be packed 327*4882a593Smuzhiyun rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json 328*4882a593Smuzhiyun rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json 329*4882a593Smuzhiyun 330*4882a593Smuzhiyun # node(1) is using /usr/lib/node as default include directory and npm(1) is 331*4882a593Smuzhiyun # using /usr/lib/node_modules as install directory. Let's make both happy. 332*4882a593Smuzhiyun ln -fs node_modules ${D}/${nonarch_libdir}/node 333*4882a593Smuzhiyun} 334*4882a593Smuzhiyun 335*4882a593SmuzhiyunFILES:${PN} += " \ 336*4882a593Smuzhiyun ${bindir} \ 337*4882a593Smuzhiyun ${nonarch_libdir} \ 338*4882a593Smuzhiyun" 339*4882a593Smuzhiyun 340*4882a593SmuzhiyunEXPORT_FUNCTIONS do_configure do_compile do_install 341