blob: ba50fcac203765fa3100f1d9db176d6a10fcd6f9 [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 Williams213cb262021-08-07 19:21:33 -050022DEPENDS:prepend = "nodejs-native "
23RDEPENDS:${PN}:append:class-target = " nodejs"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024
Andrew Geisslereff27472021-10-29 15:35:00 -050025EXTRA_OENPM = ""
26
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080027NPM_INSTALL_DEV ?= "0"
Patrick Williamsc0f7c042017-02-23 20:41:17 -060028
Andrew Geisslereff27472021-10-29 15:35:00 -050029NPM_NODEDIR ?= "${RECIPE_SYSROOT_NATIVE}${prefix_native}"
30
Andrew Geissler82c905d2020-04-13 13:39:40 -050031def npm_target_arch_map(target_arch):
32 """Maps arch names to npm arch names"""
33 import re
34 if re.match("p(pc|owerpc)(|64)", target_arch):
35 return "ppc"
36 elif re.match("i.86$", target_arch):
37 return "ia32"
38 elif re.match("x86_64$", target_arch):
39 return "x64"
40 elif re.match("arm64$", target_arch):
41 return "arm"
42 return target_arch
43
44NPM_ARCH ?= "${@npm_target_arch_map(d.getVar("TARGET_ARCH"))}"
45
46NPM_PACKAGE = "${WORKDIR}/npm-package"
47NPM_CACHE = "${WORKDIR}/npm-cache"
48NPM_BUILD = "${WORKDIR}/npm-build"
49
50def npm_global_configs(d):
51 """Get the npm global configuration"""
52 configs = []
53 # Ensure no network access is done
54 configs.append(("offline", "true"))
55 configs.append(("proxy", "http://invalid"))
56 # Configure the cache directory
57 configs.append(("cache", d.getVar("NPM_CACHE")))
58 return configs
59
60def npm_pack(env, srcdir, workdir):
61 """Run 'npm pack' on a specified directory"""
62 import shlex
63 cmd = "npm pack %s" % shlex.quote(srcdir)
Andrew Geisslereff27472021-10-29 15:35:00 -050064 args = [("ignore-scripts", "true")]
65 tarball = env.run(cmd, args=args, workdir=workdir).strip("\n")
Andrew Geissler82c905d2020-04-13 13:39:40 -050066 return os.path.join(workdir, tarball)
67
68python npm_do_configure() {
69 """
70 Step one: configure the npm cache and the main npm package
71
72 Every dependencies have been fetched and patched in the source directory.
73 They have to be packed (this remove unneeded files) and added to the npm
74 cache to be available for the next step.
75
76 The main package and its associated manifest file and shrinkwrap file have
77 to be configured to take into account these cached dependencies.
78 """
79 import base64
80 import copy
81 import json
82 import re
83 import shlex
84 import tempfile
85 from bb.fetch2.npm import NpmEnvironment
86 from bb.fetch2.npm import npm_unpack
87 from bb.fetch2.npmsw import foreach_dependencies
88 from bb.progress import OutOfProgressHandler
89
90 bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
91 bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
92
93 env = NpmEnvironment(d, configs=npm_global_configs(d))
94
95 def _npm_cache_add(tarball):
96 """Run 'npm cache add' for a specified tarball"""
97 cmd = "npm cache add %s" % shlex.quote(tarball)
98 env.run(cmd)
99
100 def _npm_integrity(tarball):
101 """Return the npm integrity of a specified tarball"""
102 sha512 = bb.utils.sha512_file(tarball)
103 return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
104
105 def _npm_version(tarball):
106 """Return the version of a specified tarball"""
107 regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
108 return re.search(regex, tarball).group(1)
109
110 def _npmsw_dependency_dict(orig, deptree):
111 """
112 Return the sub dictionary in the 'orig' dictionary corresponding to the
113 'deptree' dependency tree. This function follows the shrinkwrap file
114 format.
115 """
116 ptr = orig
117 for dep in deptree:
118 if "dependencies" not in ptr:
119 ptr["dependencies"] = {}
120 ptr = ptr["dependencies"]
121 if dep not in ptr:
122 ptr[dep] = {}
123 ptr = ptr[dep]
124 return ptr
125
126 # Manage the manifest file and shrinkwrap files
127 orig_manifest_file = d.expand("${S}/package.json")
128 orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
129 cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
130 cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
131
132 with open(orig_manifest_file, "r") as f:
133 orig_manifest = json.load(f)
134
135 cached_manifest = copy.deepcopy(orig_manifest)
136 cached_manifest.pop("dependencies", None)
137 cached_manifest.pop("devDependencies", None)
138
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600139 has_shrinkwrap_file = True
Andrew Geissler82c905d2020-04-13 13:39:40 -0500140
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600141 try:
142 with open(orig_shrinkwrap_file, "r") as f:
143 orig_shrinkwrap = json.load(f)
144 except IOError:
145 has_shrinkwrap_file = False
146
147 if has_shrinkwrap_file:
148 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
149 cached_shrinkwrap.pop("dependencies", None)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500150
151 # Manage the dependencies
152 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
153 progress_total = 1 # also count the main package
154 progress_done = 0
155
156 def _count_dependency(name, params, deptree):
157 nonlocal progress_total
158 progress_total += 1
159
160 def _cache_dependency(name, params, deptree):
161 destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
162 destsuffix = os.path.join(*destsubdirs)
163 with tempfile.TemporaryDirectory() as tmpdir:
164 # Add the dependency to the npm cache
165 destdir = os.path.join(d.getVar("S"), destsuffix)
166 tarball = npm_pack(env, destdir, tmpdir)
167 _npm_cache_add(tarball)
168 # Add its signature to the cached shrinkwrap
169 dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
170 dep["version"] = _npm_version(tarball)
171 dep["integrity"] = _npm_integrity(tarball)
172 if params.get("dev", False):
173 dep["dev"] = True
174 # Display progress
175 nonlocal progress_done
176 progress_done += 1
177 progress.write("%d/%d" % (progress_done, progress_total))
178
179 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600180
181 if has_shrinkwrap_file:
182 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
183 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500184
185 # Configure the main package
186 with tempfile.TemporaryDirectory() as tmpdir:
187 tarball = npm_pack(env, d.getVar("S"), tmpdir)
188 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
189
190 # Configure the cached manifest file and cached shrinkwrap file
191 def _update_manifest(depkey):
192 for name in orig_manifest.get(depkey, {}):
193 version = cached_shrinkwrap["dependencies"][name]["version"]
194 if depkey not in cached_manifest:
195 cached_manifest[depkey] = {}
196 cached_manifest[depkey][name] = version
197
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600198 if has_shrinkwrap_file:
199 _update_manifest("dependencies")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500200
201 if dev:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600202 if has_shrinkwrap_file:
203 _update_manifest("devDependencies")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500204
205 with open(cached_manifest_file, "w") as f:
206 json.dump(cached_manifest, f, indent=2)
207
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600208 if has_shrinkwrap_file:
209 with open(cached_shrinkwrap_file, "w") as f:
210 json.dump(cached_shrinkwrap, f, indent=2)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500211}
212
213python npm_do_compile() {
214 """
215 Step two: install the npm package
216
217 Use the configured main package and the cached dependencies to run the
218 installation process. The installation is done in a directory which is
219 not the destination directory yet.
220
221 A combination of 'npm pack' and 'npm install' is used to ensure that the
222 installed files are actual copies instead of symbolic links (which is the
223 default npm behavior).
224 """
225 import shlex
226 import tempfile
227 from bb.fetch2.npm import NpmEnvironment
228
229 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
230
Andrew Geissler82c905d2020-04-13 13:39:40 -0500231 with tempfile.TemporaryDirectory() as tmpdir:
232 args = []
Andrew Geisslereff27472021-10-29 15:35:00 -0500233 configs = npm_global_configs(d)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500234
Andrew Geisslereff27472021-10-29 15:35:00 -0500235 if bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500236 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"))
Andrew Geisslereff27472021-10-29 15:35:00 -0500250 configs.append(("nodedir", d.getVar("NPM_NODEDIR")))
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600251 configs.append(("python", d.getVar("PYTHON")))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500252
Andrew Geisslereff27472021-10-29 15:35:00 -0500253 env = NpmEnvironment(d, configs)
254
Andrew Geissler82c905d2020-04-13 13:39:40 -0500255 # Add node-pre-gyp configuration
256 args.append(("target_arch", d.getVar("NPM_ARCH")))
257 args.append(("build-from-source", "true"))
258
259 # Pack and install the main package
260 tarball = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir)
Andrew Geisslereff27472021-10-29 15:35:00 -0500261 cmd = "npm install %s %s" % (shlex.quote(tarball), d.getVar("EXTRA_OENPM"))
262 env.run(cmd, args=args)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500263}
264
265npm_do_install() {
Andrew Geissler82c905d2020-04-13 13:39:40 -0500266 # Step three: final install
267 #
268 # The previous installation have to be filtered to remove some extra files.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500269
Andrew Geissler82c905d2020-04-13 13:39:40 -0500270 rm -rf ${D}
271
272 # Copy the entire lib and bin directories
273 install -d ${D}/${nonarch_libdir}
274 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
275
276 if [ -d "${NPM_BUILD}/bin" ]
277 then
278 install -d ${D}/${bindir}
279 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
280 fi
281
282 # If the package (or its dependencies) uses node-gyp to build native addons,
283 # object files, static libraries or other temporary files can be hidden in
284 # the lib directory. To reduce the package size and to avoid QA issues
285 # (staticdev with static library files) these files must be removed.
286 local GYP_REGEX=".*/build/Release/[^/]*.node"
287
288 # Remove any node-gyp directory in ${D} to remove temporary build files
289 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
290 do
291 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
292
293 rm --recursive --force ${GYP_D_DIR}
294 done
295
296 # Copy only the node-gyp release files
297 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
298 do
299 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
300
301 install -d ${GYP_D_FILE%/*}
302 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
303 done
304
305 # Remove the shrinkwrap file which does not need to be packed
306 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
307 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
308
309 # node(1) is using /usr/lib/node as default include directory and npm(1) is
310 # using /usr/lib/node_modules as install directory. Let's make both happy.
311 ln -fs node_modules ${D}/${nonarch_libdir}/node
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500312}
313
Patrick Williams213cb262021-08-07 19:21:33 -0500314FILES:${PN} += " \
Brad Bishop15ae2502019-06-18 21:44:24 -0400315 ${bindir} \
Andrew Geissler82c905d2020-04-13 13:39:40 -0500316 ${nonarch_libdir} \
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500317"
318
Andrew Geissler82c905d2020-04-13 13:39:40 -0500319EXPORT_FUNCTIONS do_configure do_compile do_install