blob: 91da3295f2f5d7ba23a179ce8e676b8567cbbf3e [file] [log] [blame]
Patrick Williams92b42cb2022-09-03 06:53:57 -05001# 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 base = j['name'].split('/')[-1]
78 tarball = os.path.join(workdir, "%s-%s.tgz" % (base, j['version']));
79
80 # TODO: real 'npm pack' does not include directories while 'tar'
81 # does. But this does not seem to matter...
82 subprocess.run(['tar', 'czf', tarball,
83 '--exclude', './node-modules',
84 '--exclude-vcs',
Andrew Geisslerfc113ea2023-03-31 09:59:46 -050085 '--transform', r's,^\./,package/,',
Patrick Williams92b42cb2022-09-03 06:53:57 -050086 '--mtime', '1985-10-26T08:15:00.000Z',
87 '.'],
88 check = True, cwd = srcdir)
89
90 return (tarball, j)
91
92python npm_do_configure() {
93 """
94 Step one: configure the npm cache and the main npm package
95
96 Every dependencies have been fetched and patched in the source directory.
97 They have to be packed (this remove unneeded files) and added to the npm
98 cache to be available for the next step.
99
100 The main package and its associated manifest file and shrinkwrap file have
101 to be configured to take into account these cached dependencies.
102 """
103 import base64
104 import copy
105 import json
106 import re
107 import shlex
108 import stat
109 import tempfile
110 from bb.fetch2.npm import NpmEnvironment
111 from bb.fetch2.npm import npm_unpack
Andrew Geissler8f840682023-07-21 09:09:43 -0500112 from bb.fetch2.npm import npm_package
Patrick Williams92b42cb2022-09-03 06:53:57 -0500113 from bb.fetch2.npmsw import foreach_dependencies
114 from bb.progress import OutOfProgressHandler
115 from oe.npm_registry import NpmRegistry
116
117 bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
118 bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
119
120 env = NpmEnvironment(d, configs=npm_global_configs(d))
121 registry = NpmRegistry(d.getVar('NPM_REGISTRY'), d.getVar('NPM_CACHE'))
122
123 def _npm_cache_add(tarball, pkg):
124 """Add tarball to local registry and register it in the
125 cache"""
126 registry.add_pkg(tarball, pkg)
127
128 def _npm_integrity(tarball):
129 """Return the npm integrity of a specified tarball"""
130 sha512 = bb.utils.sha512_file(tarball)
131 return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
132
Patrick Williams92b42cb2022-09-03 06:53:57 -0500133 # Manage the manifest file and shrinkwrap files
134 orig_manifest_file = d.expand("${S}/package.json")
135 orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
136 cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
137 cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
138
139 with open(orig_manifest_file, "r") as f:
140 orig_manifest = json.load(f)
141
142 cached_manifest = copy.deepcopy(orig_manifest)
143 cached_manifest.pop("dependencies", None)
144 cached_manifest.pop("devDependencies", None)
145
146 has_shrinkwrap_file = True
147
148 try:
149 with open(orig_shrinkwrap_file, "r") as f:
150 orig_shrinkwrap = json.load(f)
151 except IOError:
152 has_shrinkwrap_file = False
153
154 if has_shrinkwrap_file:
155 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
Andrew Geissler8f840682023-07-21 09:09:43 -0500156 for package in orig_shrinkwrap["packages"]:
157 if package != "":
158 cached_shrinkwrap["packages"].pop(package, None)
159 cached_shrinkwrap["packages"][""].pop("dependencies", None)
160 cached_shrinkwrap["packages"][""].pop("devDependencies", None)
161 cached_shrinkwrap["packages"][""].pop("peerDependencies", None)
Patrick Williams92b42cb2022-09-03 06:53:57 -0500162
163 # Manage the dependencies
164 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
165 progress_total = 1 # also count the main package
166 progress_done = 0
167
Andrew Geissler8f840682023-07-21 09:09:43 -0500168 def _count_dependency(name, params, destsuffix):
Patrick Williams92b42cb2022-09-03 06:53:57 -0500169 nonlocal progress_total
170 progress_total += 1
171
Andrew Geissler8f840682023-07-21 09:09:43 -0500172 def _cache_dependency(name, params, destsuffix):
Patrick Williams92b42cb2022-09-03 06:53:57 -0500173 with tempfile.TemporaryDirectory() as tmpdir:
174 # Add the dependency to the npm cache
175 destdir = os.path.join(d.getVar("S"), destsuffix)
176 (tarball, pkg) = npm_pack(env, destdir, tmpdir)
177 _npm_cache_add(tarball, pkg)
178 # Add its signature to the cached shrinkwrap
Andrew Geissler8f840682023-07-21 09:09:43 -0500179 dep = params
Patrick Williams92b42cb2022-09-03 06:53:57 -0500180 dep["version"] = pkg['version']
181 dep["integrity"] = _npm_integrity(tarball)
182 if params.get("dev", False):
183 dep["dev"] = True
Andrew Geissler8f840682023-07-21 09:09:43 -0500184 if "devDependencies" not in cached_shrinkwrap["packages"][""]:
185 cached_shrinkwrap["packages"][""]["devDependencies"] = {}
186 cached_shrinkwrap["packages"][""]["devDependencies"][name] = pkg['version']
187
188 else:
189 if "dependencies" not in cached_shrinkwrap["packages"][""]:
190 cached_shrinkwrap["packages"][""]["dependencies"] = {}
191 cached_shrinkwrap["packages"][""]["dependencies"][name] = pkg['version']
192
193 cached_shrinkwrap["packages"][destsuffix] = dep
Patrick Williams92b42cb2022-09-03 06:53:57 -0500194 # Display progress
195 nonlocal progress_done
196 progress_done += 1
197 progress.write("%d/%d" % (progress_done, progress_total))
198
199 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
200
201 if has_shrinkwrap_file:
202 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
203 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
Andrew Geissler8f840682023-07-21 09:09:43 -0500204
205 # Manage Peer Dependencies
206 if has_shrinkwrap_file:
207 packages = orig_shrinkwrap.get("packages", {})
208 peer_deps = packages.get("", {}).get("peerDependencies", {})
209 package_runtime_dependencies = d.getVar("RDEPENDS:%s" % d.getVar("PN"))
210
211 for peer_dep in peer_deps:
212 peer_dep_yocto_name = npm_package(peer_dep)
213 if peer_dep_yocto_name not in package_runtime_dependencies:
214 bb.warn(peer_dep + " is a peer dependencie that is not in RDEPENDS variable. " +
215 "Please add this peer dependencie to the RDEPENDS variable as %s and generate its recipe with devtool"
216 % peer_dep_yocto_name)
Patrick Williams92b42cb2022-09-03 06:53:57 -0500217
218 # Configure the main package
219 with tempfile.TemporaryDirectory() as tmpdir:
220 (tarball, _) = npm_pack(env, d.getVar("S"), tmpdir)
221 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
222
223 # Configure the cached manifest file and cached shrinkwrap file
224 def _update_manifest(depkey):
225 for name in orig_manifest.get(depkey, {}):
Andrew Geissler8f840682023-07-21 09:09:43 -0500226 version = cached_shrinkwrap["packages"][""][depkey][name]
Patrick Williams92b42cb2022-09-03 06:53:57 -0500227 if depkey not in cached_manifest:
228 cached_manifest[depkey] = {}
229 cached_manifest[depkey][name] = version
230
231 if has_shrinkwrap_file:
232 _update_manifest("dependencies")
233
234 if dev:
235 if has_shrinkwrap_file:
236 _update_manifest("devDependencies")
237
238 os.chmod(cached_manifest_file, os.stat(cached_manifest_file).st_mode | stat.S_IWUSR)
239 with open(cached_manifest_file, "w") as f:
240 json.dump(cached_manifest, f, indent=2)
241
242 if has_shrinkwrap_file:
243 with open(cached_shrinkwrap_file, "w") as f:
244 json.dump(cached_shrinkwrap, f, indent=2)
245}
246
247python npm_do_compile() {
248 """
249 Step two: install the npm package
250
251 Use the configured main package and the cached dependencies to run the
252 installation process. The installation is done in a directory which is
253 not the destination directory yet.
254
255 A combination of 'npm pack' and 'npm install' is used to ensure that the
256 installed files are actual copies instead of symbolic links (which is the
257 default npm behavior).
258 """
259 import shlex
260 import tempfile
261 from bb.fetch2.npm import NpmEnvironment
262
263 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
264
265 with tempfile.TemporaryDirectory() as tmpdir:
266 args = []
267 configs = npm_global_configs(d)
268
269 if bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False):
270 configs.append(("also", "development"))
271 else:
272 configs.append(("only", "production"))
273
274 # Report as many logs as possible for debugging purpose
275 configs.append(("loglevel", "silly"))
276
277 # Configure the installation to be done globally in the build directory
278 configs.append(("global", "true"))
279 configs.append(("prefix", d.getVar("NPM_BUILD")))
280
281 # Add node-gyp configuration
282 configs.append(("arch", d.getVar("NPM_ARCH")))
283 configs.append(("release", "true"))
284 configs.append(("nodedir", d.getVar("NPM_NODEDIR")))
285 configs.append(("python", d.getVar("PYTHON")))
286
287 env = NpmEnvironment(d, configs)
288
289 # Add node-pre-gyp configuration
290 args.append(("target_arch", d.getVar("NPM_ARCH")))
291 args.append(("build-from-source", "true"))
292
Andrew Geissler8f840682023-07-21 09:09:43 -0500293 # Don't install peer dependencies as they should be in RDEPENDS variable
294 args.append(("legacy-peer-deps", "true"))
295
Patrick Williams92b42cb2022-09-03 06:53:57 -0500296 # Pack and install the main package
297 (tarball, _) = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir)
298 cmd = "npm install %s %s" % (shlex.quote(tarball), d.getVar("EXTRA_OENPM"))
299 env.run(cmd, args=args)
300}
301
302npm_do_install() {
303 # Step three: final install
304 #
305 # The previous installation have to be filtered to remove some extra files.
306
307 rm -rf ${D}
308
309 # Copy the entire lib and bin directories
310 install -d ${D}/${nonarch_libdir}
311 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
312
313 if [ -d "${NPM_BUILD}/bin" ]
314 then
315 install -d ${D}/${bindir}
316 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
317 fi
318
319 # If the package (or its dependencies) uses node-gyp to build native addons,
320 # object files, static libraries or other temporary files can be hidden in
321 # the lib directory. To reduce the package size and to avoid QA issues
322 # (staticdev with static library files) these files must be removed.
323 local GYP_REGEX=".*/build/Release/[^/]*.node"
324
325 # Remove any node-gyp directory in ${D} to remove temporary build files
326 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
327 do
328 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
329
330 rm --recursive --force ${GYP_D_DIR}
331 done
332
333 # Copy only the node-gyp release files
334 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
335 do
336 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
337
338 install -d ${GYP_D_FILE%/*}
339 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
340 done
341
342 # Remove the shrinkwrap file which does not need to be packed
343 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
344 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
345}
346
347FILES:${PN} += " \
348 ${bindir} \
349 ${nonarch_libdir} \
350"
351
352EXPORT_FUNCTIONS do_configure do_compile do_install