blob: bd01e247cda56ba927b5e1eec8feff91092d6a72 [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
Patrick Williams776d5d22021-03-11 14:29:47 -060020inherit python3native
21
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022DEPENDS_prepend = "nodejs-native "
Patrick Williams776d5d22021-03-11 14:29:47 -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
135 with open(orig_shrinkwrap_file, "r") as f:
136 orig_shrinkwrap = json.load(f)
137
138 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
139 cached_shrinkwrap.pop("dependencies", None)
140
141 # Manage the dependencies
142 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
143 progress_total = 1 # also count the main package
144 progress_done = 0
145
146 def _count_dependency(name, params, deptree):
147 nonlocal progress_total
148 progress_total += 1
149
150 def _cache_dependency(name, params, deptree):
151 destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
152 destsuffix = os.path.join(*destsubdirs)
153 with tempfile.TemporaryDirectory() as tmpdir:
154 # Add the dependency to the npm cache
155 destdir = os.path.join(d.getVar("S"), destsuffix)
156 tarball = npm_pack(env, destdir, tmpdir)
157 _npm_cache_add(tarball)
158 # Add its signature to the cached shrinkwrap
159 dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
160 dep["version"] = _npm_version(tarball)
161 dep["integrity"] = _npm_integrity(tarball)
162 if params.get("dev", False):
163 dep["dev"] = True
164 # Display progress
165 nonlocal progress_done
166 progress_done += 1
167 progress.write("%d/%d" % (progress_done, progress_total))
168
169 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
170 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
171 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
172
173 # Configure the main package
174 with tempfile.TemporaryDirectory() as tmpdir:
175 tarball = npm_pack(env, d.getVar("S"), tmpdir)
176 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
177
178 # Configure the cached manifest file and cached shrinkwrap file
179 def _update_manifest(depkey):
180 for name in orig_manifest.get(depkey, {}):
181 version = cached_shrinkwrap["dependencies"][name]["version"]
182 if depkey not in cached_manifest:
183 cached_manifest[depkey] = {}
184 cached_manifest[depkey][name] = version
185
186 _update_manifest("dependencies")
187
188 if dev:
189 _update_manifest("devDependencies")
190
191 with open(cached_manifest_file, "w") as f:
192 json.dump(cached_manifest, f, indent=2)
193
194 with open(cached_shrinkwrap_file, "w") as f:
195 json.dump(cached_shrinkwrap, f, indent=2)
196}
197
198python npm_do_compile() {
199 """
200 Step two: install the npm package
201
202 Use the configured main package and the cached dependencies to run the
203 installation process. The installation is done in a directory which is
204 not the destination directory yet.
205
206 A combination of 'npm pack' and 'npm install' is used to ensure that the
207 installed files are actual copies instead of symbolic links (which is the
208 default npm behavior).
209 """
210 import shlex
211 import tempfile
212 from bb.fetch2.npm import NpmEnvironment
213
214 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
215
216 env = NpmEnvironment(d, configs=npm_global_configs(d))
217
218 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
219
220 with tempfile.TemporaryDirectory() as tmpdir:
221 args = []
222 configs = []
223
224 if dev:
225 configs.append(("also", "development"))
226 else:
227 configs.append(("only", "production"))
228
229 # Report as many logs as possible for debugging purpose
230 configs.append(("loglevel", "silly"))
231
232 # Configure the installation to be done globally in the build directory
233 configs.append(("global", "true"))
234 configs.append(("prefix", d.getVar("NPM_BUILD")))
235
236 # Add node-gyp configuration
237 configs.append(("arch", d.getVar("NPM_ARCH")))
238 configs.append(("release", "true"))
239 sysroot = d.getVar("RECIPE_SYSROOT_NATIVE")
240 nodedir = os.path.join(sysroot, d.getVar("prefix_native").strip("/"))
241 configs.append(("nodedir", nodedir))
Patrick Williams776d5d22021-03-11 14:29:47 -0600242 configs.append(("python", d.getVar("PYTHON")))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500243
244 # Add node-pre-gyp configuration
245 args.append(("target_arch", d.getVar("NPM_ARCH")))
246 args.append(("build-from-source", "true"))
247
248 # Pack and install the main package
249 tarball = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir)
250 env.run("npm install %s" % shlex.quote(tarball), args=args, configs=configs)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500251}
252
253npm_do_install() {
Andrew Geissler82c905d2020-04-13 13:39:40 -0500254 # Step three: final install
255 #
256 # The previous installation have to be filtered to remove some extra files.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500257
Andrew Geissler82c905d2020-04-13 13:39:40 -0500258 rm -rf ${D}
259
260 # Copy the entire lib and bin directories
261 install -d ${D}/${nonarch_libdir}
262 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
263
264 if [ -d "${NPM_BUILD}/bin" ]
265 then
266 install -d ${D}/${bindir}
267 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
268 fi
269
270 # If the package (or its dependencies) uses node-gyp to build native addons,
271 # object files, static libraries or other temporary files can be hidden in
272 # the lib directory. To reduce the package size and to avoid QA issues
273 # (staticdev with static library files) these files must be removed.
274 local GYP_REGEX=".*/build/Release/[^/]*.node"
275
276 # Remove any node-gyp directory in ${D} to remove temporary build files
277 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
278 do
279 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
280
281 rm --recursive --force ${GYP_D_DIR}
282 done
283
284 # Copy only the node-gyp release files
285 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
286 do
287 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
288
289 install -d ${GYP_D_FILE%/*}
290 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
291 done
292
293 # Remove the shrinkwrap file which does not need to be packed
294 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
295 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
296
297 # node(1) is using /usr/lib/node as default include directory and npm(1) is
298 # using /usr/lib/node_modules as install directory. Let's make both happy.
299 ln -fs node_modules ${D}/${nonarch_libdir}/node
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500300}
301
302FILES_${PN} += " \
Brad Bishop15ae2502019-06-18 21:44:24 -0400303 ${bindir} \
Andrew Geissler82c905d2020-04-13 13:39:40 -0500304 ${nonarch_libdir} \
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500305"
306
Andrew Geissler82c905d2020-04-13 13:39:40 -0500307EXPORT_FUNCTIONS do_configure do_compile do_install