blob: 068032a1e57b2bd973c2d75e7b6537999735b6b0 [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 Williamsd8c66bc2016-06-20 12:57:21 -050020DEPENDS_prepend = "nodejs-native "
Patrick Williamsc0f7c042017-02-23 20:41:17 -060021RDEPENDS_${PN}_prepend = "nodejs "
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080023NPM_INSTALL_DEV ?= "0"
Patrick Williamsc0f7c042017-02-23 20:41:17 -060024
Andrew Geissler82c905d2020-04-13 13:39:40 -050025def npm_target_arch_map(target_arch):
26 """Maps arch names to npm arch names"""
27 import re
28 if re.match("p(pc|owerpc)(|64)", target_arch):
29 return "ppc"
30 elif re.match("i.86$", target_arch):
31 return "ia32"
32 elif re.match("x86_64$", target_arch):
33 return "x64"
34 elif re.match("arm64$", target_arch):
35 return "arm"
36 return target_arch
37
38NPM_ARCH ?= "${@npm_target_arch_map(d.getVar("TARGET_ARCH"))}"
39
40NPM_PACKAGE = "${WORKDIR}/npm-package"
41NPM_CACHE = "${WORKDIR}/npm-cache"
42NPM_BUILD = "${WORKDIR}/npm-build"
43
44def npm_global_configs(d):
45 """Get the npm global configuration"""
46 configs = []
47 # Ensure no network access is done
48 configs.append(("offline", "true"))
49 configs.append(("proxy", "http://invalid"))
50 # Configure the cache directory
51 configs.append(("cache", d.getVar("NPM_CACHE")))
52 return configs
53
54def npm_pack(env, srcdir, workdir):
55 """Run 'npm pack' on a specified directory"""
56 import shlex
57 cmd = "npm pack %s" % shlex.quote(srcdir)
58 configs = [("ignore-scripts", "true")]
59 tarball = env.run(cmd, configs=configs, workdir=workdir).strip("\n")
60 return os.path.join(workdir, tarball)
61
62python npm_do_configure() {
63 """
64 Step one: configure the npm cache and the main npm package
65
66 Every dependencies have been fetched and patched in the source directory.
67 They have to be packed (this remove unneeded files) and added to the npm
68 cache to be available for the next step.
69
70 The main package and its associated manifest file and shrinkwrap file have
71 to be configured to take into account these cached dependencies.
72 """
73 import base64
74 import copy
75 import json
76 import re
77 import shlex
78 import tempfile
79 from bb.fetch2.npm import NpmEnvironment
80 from bb.fetch2.npm import npm_unpack
81 from bb.fetch2.npmsw import foreach_dependencies
82 from bb.progress import OutOfProgressHandler
83
84 bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
85 bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
86
87 env = NpmEnvironment(d, configs=npm_global_configs(d))
88
89 def _npm_cache_add(tarball):
90 """Run 'npm cache add' for a specified tarball"""
91 cmd = "npm cache add %s" % shlex.quote(tarball)
92 env.run(cmd)
93
94 def _npm_integrity(tarball):
95 """Return the npm integrity of a specified tarball"""
96 sha512 = bb.utils.sha512_file(tarball)
97 return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
98
99 def _npm_version(tarball):
100 """Return the version of a specified tarball"""
101 regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
102 return re.search(regex, tarball).group(1)
103
104 def _npmsw_dependency_dict(orig, deptree):
105 """
106 Return the sub dictionary in the 'orig' dictionary corresponding to the
107 'deptree' dependency tree. This function follows the shrinkwrap file
108 format.
109 """
110 ptr = orig
111 for dep in deptree:
112 if "dependencies" not in ptr:
113 ptr["dependencies"] = {}
114 ptr = ptr["dependencies"]
115 if dep not in ptr:
116 ptr[dep] = {}
117 ptr = ptr[dep]
118 return ptr
119
120 # Manage the manifest file and shrinkwrap files
121 orig_manifest_file = d.expand("${S}/package.json")
122 orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
123 cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
124 cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
125
126 with open(orig_manifest_file, "r") as f:
127 orig_manifest = json.load(f)
128
129 cached_manifest = copy.deepcopy(orig_manifest)
130 cached_manifest.pop("dependencies", None)
131 cached_manifest.pop("devDependencies", None)
132
133 with open(orig_shrinkwrap_file, "r") as f:
134 orig_shrinkwrap = json.load(f)
135
136 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
137 cached_shrinkwrap.pop("dependencies", None)
138
139 # Manage the dependencies
140 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
141 progress_total = 1 # also count the main package
142 progress_done = 0
143
144 def _count_dependency(name, params, deptree):
145 nonlocal progress_total
146 progress_total += 1
147
148 def _cache_dependency(name, params, deptree):
149 destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
150 destsuffix = os.path.join(*destsubdirs)
151 with tempfile.TemporaryDirectory() as tmpdir:
152 # Add the dependency to the npm cache
153 destdir = os.path.join(d.getVar("S"), destsuffix)
154 tarball = npm_pack(env, destdir, tmpdir)
155 _npm_cache_add(tarball)
156 # Add its signature to the cached shrinkwrap
157 dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
158 dep["version"] = _npm_version(tarball)
159 dep["integrity"] = _npm_integrity(tarball)
160 if params.get("dev", False):
161 dep["dev"] = True
162 # Display progress
163 nonlocal progress_done
164 progress_done += 1
165 progress.write("%d/%d" % (progress_done, progress_total))
166
167 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
168 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
169 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
170
171 # Configure the main package
172 with tempfile.TemporaryDirectory() as tmpdir:
173 tarball = npm_pack(env, d.getVar("S"), tmpdir)
174 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
175
176 # Configure the cached manifest file and cached shrinkwrap file
177 def _update_manifest(depkey):
178 for name in orig_manifest.get(depkey, {}):
179 version = cached_shrinkwrap["dependencies"][name]["version"]
180 if depkey not in cached_manifest:
181 cached_manifest[depkey] = {}
182 cached_manifest[depkey][name] = version
183
184 _update_manifest("dependencies")
185
186 if dev:
187 _update_manifest("devDependencies")
188
189 with open(cached_manifest_file, "w") as f:
190 json.dump(cached_manifest, f, indent=2)
191
192 with open(cached_shrinkwrap_file, "w") as f:
193 json.dump(cached_shrinkwrap, f, indent=2)
194}
195
196python npm_do_compile() {
197 """
198 Step two: install the npm package
199
200 Use the configured main package and the cached dependencies to run the
201 installation process. The installation is done in a directory which is
202 not the destination directory yet.
203
204 A combination of 'npm pack' and 'npm install' is used to ensure that the
205 installed files are actual copies instead of symbolic links (which is the
206 default npm behavior).
207 """
208 import shlex
209 import tempfile
210 from bb.fetch2.npm import NpmEnvironment
211
212 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
213
214 env = NpmEnvironment(d, configs=npm_global_configs(d))
215
216 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
217
218 with tempfile.TemporaryDirectory() as tmpdir:
219 args = []
220 configs = []
221
222 if dev:
223 configs.append(("also", "development"))
224 else:
225 configs.append(("only", "production"))
226
227 # Report as many logs as possible for debugging purpose
228 configs.append(("loglevel", "silly"))
229
230 # Configure the installation to be done globally in the build directory
231 configs.append(("global", "true"))
232 configs.append(("prefix", d.getVar("NPM_BUILD")))
233
234 # Add node-gyp configuration
235 configs.append(("arch", d.getVar("NPM_ARCH")))
236 configs.append(("release", "true"))
237 sysroot = d.getVar("RECIPE_SYSROOT_NATIVE")
238 nodedir = os.path.join(sysroot, d.getVar("prefix_native").strip("/"))
239 configs.append(("nodedir", nodedir))
240 bindir = os.path.join(sysroot, d.getVar("bindir_native").strip("/"))
241 pythondir = os.path.join(bindir, "python-native", "python")
242 configs.append(("python", pythondir))
243
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