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