blob: 8f8712a0242fe0473f38e9245845229bfae1a6f5 [file] [log] [blame]
Andrew Geissler82c905d2020-04-13 13:39:40 -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
Andrew Geisslerd1e89492021-02-12 15:35:20 -060020inherit python3native
21
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022DEPENDS_prepend = "nodejs-native "
Andrew Geissler9b4d8b02021-02-19 12:26:16 -060023RDEPENDS_${PN}_append_class-target = " nodejs"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080025NPM_INSTALL_DEV ?= "0"
Patrick Williamsc0f7c042017-02-23 20:41:17 -060026
Andrew Geissler82c905d2020-04-13 13:39:40 -050027def npm_target_arch_map(target_arch):
28 """Maps arch names to npm arch names"""
29 import re
30 if re.match("p(pc|owerpc)(|64)", target_arch):
31 return "ppc"
32 elif re.match("i.86$", target_arch):
33 return "ia32"
34 elif re.match("x86_64$", target_arch):
35 return "x64"
36 elif re.match("arm64$", target_arch):
37 return "arm"
38 return target_arch
39
40NPM_ARCH ?= "${@npm_target_arch_map(d.getVar("TARGET_ARCH"))}"
41
42NPM_PACKAGE = "${WORKDIR}/npm-package"
43NPM_CACHE = "${WORKDIR}/npm-cache"
44NPM_BUILD = "${WORKDIR}/npm-build"
45
46def npm_global_configs(d):
47 """Get the npm global configuration"""
48 configs = []
49 # Ensure no network access is done
50 configs.append(("offline", "true"))
51 configs.append(("proxy", "http://invalid"))
52 # Configure the cache directory
53 configs.append(("cache", d.getVar("NPM_CACHE")))
54 return configs
55
56def npm_pack(env, srcdir, workdir):
57 """Run 'npm pack' on a specified directory"""
58 import shlex
59 cmd = "npm pack %s" % shlex.quote(srcdir)
60 configs = [("ignore-scripts", "true")]
61 tarball = env.run(cmd, configs=configs, workdir=workdir).strip("\n")
62 return os.path.join(workdir, tarball)
63
64python npm_do_configure() {
65 """
66 Step one: configure the npm cache and the main npm package
67
68 Every dependencies have been fetched and patched in the source directory.
69 They have to be packed (this remove unneeded files) and added to the npm
70 cache to be available for the next step.
71
72 The main package and its associated manifest file and shrinkwrap file have
73 to be configured to take into account these cached dependencies.
74 """
75 import base64
76 import copy
77 import json
78 import re
79 import shlex
80 import tempfile
81 from bb.fetch2.npm import NpmEnvironment
82 from bb.fetch2.npm import npm_unpack
83 from bb.fetch2.npmsw import foreach_dependencies
84 from bb.progress import OutOfProgressHandler
85
86 bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
87 bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
88
89 env = NpmEnvironment(d, configs=npm_global_configs(d))
90
91 def _npm_cache_add(tarball):
92 """Run 'npm cache add' for a specified tarball"""
93 cmd = "npm cache add %s" % shlex.quote(tarball)
94 env.run(cmd)
95
96 def _npm_integrity(tarball):
97 """Return the npm integrity of a specified tarball"""
98 sha512 = bb.utils.sha512_file(tarball)
99 return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
100
101 def _npm_version(tarball):
102 """Return the version of a specified tarball"""
103 regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
104 return re.search(regex, tarball).group(1)
105
106 def _npmsw_dependency_dict(orig, deptree):
107 """
108 Return the sub dictionary in the 'orig' dictionary corresponding to the
109 'deptree' dependency tree. This function follows the shrinkwrap file
110 format.
111 """
112 ptr = orig
113 for dep in deptree:
114 if "dependencies" not in ptr:
115 ptr["dependencies"] = {}
116 ptr = ptr["dependencies"]
117 if dep not in ptr:
118 ptr[dep] = {}
119 ptr = ptr[dep]
120 return ptr
121
122 # Manage the manifest file and shrinkwrap files
123 orig_manifest_file = d.expand("${S}/package.json")
124 orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
125 cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
126 cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
127
128 with open(orig_manifest_file, "r") as f:
129 orig_manifest = json.load(f)
130
131 cached_manifest = copy.deepcopy(orig_manifest)
132 cached_manifest.pop("dependencies", None)
133 cached_manifest.pop("devDependencies", None)
134
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600135 has_shrinkwrap_file = True
Andrew Geissler82c905d2020-04-13 13:39:40 -0500136
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600137 try:
138 with open(orig_shrinkwrap_file, "r") as f:
139 orig_shrinkwrap = json.load(f)
140 except IOError:
141 has_shrinkwrap_file = False
142
143 if has_shrinkwrap_file:
144 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
145 cached_shrinkwrap.pop("dependencies", None)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500146
147 # Manage the dependencies
148 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
149 progress_total = 1 # also count the main package
150 progress_done = 0
151
152 def _count_dependency(name, params, deptree):
153 nonlocal progress_total
154 progress_total += 1
155
156 def _cache_dependency(name, params, deptree):
157 destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
158 destsuffix = os.path.join(*destsubdirs)
159 with tempfile.TemporaryDirectory() as tmpdir:
160 # Add the dependency to the npm cache
161 destdir = os.path.join(d.getVar("S"), destsuffix)
162 tarball = npm_pack(env, destdir, tmpdir)
163 _npm_cache_add(tarball)
164 # Add its signature to the cached shrinkwrap
165 dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
166 dep["version"] = _npm_version(tarball)
167 dep["integrity"] = _npm_integrity(tarball)
168 if params.get("dev", False):
169 dep["dev"] = True
170 # Display progress
171 nonlocal progress_done
172 progress_done += 1
173 progress.write("%d/%d" % (progress_done, progress_total))
174
175 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600176
177 if has_shrinkwrap_file:
178 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
179 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500180
181 # Configure the main package
182 with tempfile.TemporaryDirectory() as tmpdir:
183 tarball = npm_pack(env, d.getVar("S"), tmpdir)
184 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
185
186 # Configure the cached manifest file and cached shrinkwrap file
187 def _update_manifest(depkey):
188 for name in orig_manifest.get(depkey, {}):
189 version = cached_shrinkwrap["dependencies"][name]["version"]
190 if depkey not in cached_manifest:
191 cached_manifest[depkey] = {}
192 cached_manifest[depkey][name] = version
193
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600194 if has_shrinkwrap_file:
195 _update_manifest("dependencies")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500196
197 if dev:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600198 if has_shrinkwrap_file:
199 _update_manifest("devDependencies")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500200
201 with open(cached_manifest_file, "w") as f:
202 json.dump(cached_manifest, f, indent=2)
203
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600204 if has_shrinkwrap_file:
205 with open(cached_shrinkwrap_file, "w") as f:
206 json.dump(cached_shrinkwrap, f, indent=2)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500207}
208
209python npm_do_compile() {
210 """
211 Step two: install the npm package
212
213 Use the configured main package and the cached dependencies to run the
214 installation process. The installation is done in a directory which is
215 not the destination directory yet.
216
217 A combination of 'npm pack' and 'npm install' is used to ensure that the
218 installed files are actual copies instead of symbolic links (which is the
219 default npm behavior).
220 """
221 import shlex
222 import tempfile
223 from bb.fetch2.npm import NpmEnvironment
224
225 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
226
227 env = NpmEnvironment(d, configs=npm_global_configs(d))
228
229 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
230
231 with tempfile.TemporaryDirectory() as tmpdir:
232 args = []
233 configs = []
234
235 if dev:
236 configs.append(("also", "development"))
237 else:
238 configs.append(("only", "production"))
239
240 # Report as many logs as possible for debugging purpose
241 configs.append(("loglevel", "silly"))
242
243 # Configure the installation to be done globally in the build directory
244 configs.append(("global", "true"))
245 configs.append(("prefix", d.getVar("NPM_BUILD")))
246
247 # Add node-gyp configuration
248 configs.append(("arch", d.getVar("NPM_ARCH")))
249 configs.append(("release", "true"))
William A. Kennington IIIac69b482021-06-02 12:28:27 -0700250 nodedir = d.getVar("NPM_NODEDIR")
251 if not nodedir:
252 sysroot = d.getVar("RECIPE_SYSROOT_NATIVE")
253 nodedir = os.path.join(sysroot, d.getVar("prefix_native").strip("/"))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500254 configs.append(("nodedir", nodedir))
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600255 configs.append(("python", d.getVar("PYTHON")))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500256
257 # Add node-pre-gyp configuration
258 args.append(("target_arch", d.getVar("NPM_ARCH")))
259 args.append(("build-from-source", "true"))
260
261 # Pack and install the main package
262 tarball = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir)
263 env.run("npm install %s" % shlex.quote(tarball), args=args, configs=configs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500264}
265
266npm_do_install() {
Andrew Geissler82c905d2020-04-13 13:39:40 -0500267 # Step three: final install
268 #
269 # The previous installation have to be filtered to remove some extra files.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500270
Andrew Geissler82c905d2020-04-13 13:39:40 -0500271 rm -rf ${D}
272
273 # Copy the entire lib and bin directories
274 install -d ${D}/${nonarch_libdir}
275 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
276
277 if [ -d "${NPM_BUILD}/bin" ]
278 then
279 install -d ${D}/${bindir}
280 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
281 fi
282
283 # If the package (or its dependencies) uses node-gyp to build native addons,
284 # object files, static libraries or other temporary files can be hidden in
285 # the lib directory. To reduce the package size and to avoid QA issues
286 # (staticdev with static library files) these files must be removed.
287 local GYP_REGEX=".*/build/Release/[^/]*.node"
288
289 # Remove any node-gyp directory in ${D} to remove temporary build files
290 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
291 do
292 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
293
294 rm --recursive --force ${GYP_D_DIR}
295 done
296
297 # Copy only the node-gyp release files
298 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
299 do
300 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
301
302 install -d ${GYP_D_FILE%/*}
303 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
304 done
305
306 # Remove the shrinkwrap file which does not need to be packed
307 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
308 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
309
310 # node(1) is using /usr/lib/node as default include directory and npm(1) is
311 # using /usr/lib/node_modules as install directory. Let's make both happy.
312 ln -fs node_modules ${D}/${nonarch_libdir}/node
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500313}
314
315FILES_${PN} += " \
Brad Bishop15ae2502019-06-18 21:44:24 -0400316 ${bindir} \
Andrew Geissler82c905d2020-04-13 13:39:40 -0500317 ${nonarch_libdir} \
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500318"
319
Andrew Geissler82c905d2020-04-13 13:39:40 -0500320EXPORT_FUNCTIONS do_configure do_compile do_install