blob: 79f55febccf2389ad5d5cacfb6b55c1f88db9c8f [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 "
Patrick Williamsc0f7c042017-02-23 20:41:17 -060023RDEPENDS_${PN}_prepend = "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"))
250 sysroot = d.getVar("RECIPE_SYSROOT_NATIVE")
251 nodedir = os.path.join(sysroot, d.getVar("prefix_native").strip("/"))
252 configs.append(("nodedir", nodedir))
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600253 configs.append(("python", d.getVar("PYTHON")))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500254
255 # 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)
261 env.run("npm install %s" % shlex.quote(tarball), args=args, configs=configs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500262}
263
264npm_do_install() {
Andrew Geissler82c905d2020-04-13 13:39:40 -0500265 # Step three: final install
266 #
267 # The previous installation have to be filtered to remove some extra files.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500268
Andrew Geissler82c905d2020-04-13 13:39:40 -0500269 rm -rf ${D}
270
271 # Copy the entire lib and bin directories
272 install -d ${D}/${nonarch_libdir}
273 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
274
275 if [ -d "${NPM_BUILD}/bin" ]
276 then
277 install -d ${D}/${bindir}
278 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
279 fi
280
281 # If the package (or its dependencies) uses node-gyp to build native addons,
282 # object files, static libraries or other temporary files can be hidden in
283 # the lib directory. To reduce the package size and to avoid QA issues
284 # (staticdev with static library files) these files must be removed.
285 local GYP_REGEX=".*/build/Release/[^/]*.node"
286
287 # Remove any node-gyp directory in ${D} to remove temporary build files
288 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
289 do
290 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
291
292 rm --recursive --force ${GYP_D_DIR}
293 done
294
295 # Copy only the node-gyp release files
296 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
297 do
298 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
299
300 install -d ${GYP_D_FILE%/*}
301 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
302 done
303
304 # Remove the shrinkwrap file which does not need to be packed
305 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
306 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
307
308 # node(1) is using /usr/lib/node as default include directory and npm(1) is
309 # using /usr/lib/node_modules as install directory. Let's make both happy.
310 ln -fs node_modules ${D}/${nonarch_libdir}/node
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500311}
312
313FILES_${PN} += " \
Brad Bishop15ae2502019-06-18 21:44:24 -0400314 ${bindir} \
Andrew Geissler82c905d2020-04-13 13:39:40 -0500315 ${nonarch_libdir} \
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500316"
317
Andrew Geissler82c905d2020-04-13 13:39:40 -0500318EXPORT_FUNCTIONS do_configure do_compile do_install