xref: /OK3568_Linux_fs/yocto/poky/meta/classes/npm.bbclass (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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