blob: dbfc2e728e7b97baaac71a7053c12b3162a70777 [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
Andrew Geisslerd5838332022-05-27 11:33:10 -050084 import stat
Andrew Geissler82c905d2020-04-13 13:39:40 -050085 import tempfile
86 from bb.fetch2.npm import NpmEnvironment
87 from bb.fetch2.npm import npm_unpack
88 from bb.fetch2.npmsw import foreach_dependencies
89 from bb.progress import OutOfProgressHandler
90
91 bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
92 bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
93
94 env = NpmEnvironment(d, configs=npm_global_configs(d))
95
96 def _npm_cache_add(tarball):
97 """Run 'npm cache add' for a specified tarball"""
98 cmd = "npm cache add %s" % shlex.quote(tarball)
99 env.run(cmd)
100
101 def _npm_integrity(tarball):
102 """Return the npm integrity of a specified tarball"""
103 sha512 = bb.utils.sha512_file(tarball)
104 return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
105
106 def _npm_version(tarball):
107 """Return the version of a specified tarball"""
108 regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
109 return re.search(regex, tarball).group(1)
110
111 def _npmsw_dependency_dict(orig, deptree):
112 """
113 Return the sub dictionary in the 'orig' dictionary corresponding to the
114 'deptree' dependency tree. This function follows the shrinkwrap file
115 format.
116 """
117 ptr = orig
118 for dep in deptree:
119 if "dependencies" not in ptr:
120 ptr["dependencies"] = {}
121 ptr = ptr["dependencies"]
122 if dep not in ptr:
123 ptr[dep] = {}
124 ptr = ptr[dep]
125 return ptr
126
127 # Manage the manifest file and shrinkwrap files
128 orig_manifest_file = d.expand("${S}/package.json")
129 orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
130 cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
131 cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
132
133 with open(orig_manifest_file, "r") as f:
134 orig_manifest = json.load(f)
135
136 cached_manifest = copy.deepcopy(orig_manifest)
137 cached_manifest.pop("dependencies", None)
138 cached_manifest.pop("devDependencies", None)
139
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600140 has_shrinkwrap_file = True
Andrew Geissler82c905d2020-04-13 13:39:40 -0500141
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600142 try:
143 with open(orig_shrinkwrap_file, "r") as f:
144 orig_shrinkwrap = json.load(f)
145 except IOError:
146 has_shrinkwrap_file = False
147
148 if has_shrinkwrap_file:
149 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
150 cached_shrinkwrap.pop("dependencies", None)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500151
152 # Manage the dependencies
153 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
154 progress_total = 1 # also count the main package
155 progress_done = 0
156
157 def _count_dependency(name, params, deptree):
158 nonlocal progress_total
159 progress_total += 1
160
161 def _cache_dependency(name, params, deptree):
162 destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
163 destsuffix = os.path.join(*destsubdirs)
164 with tempfile.TemporaryDirectory() as tmpdir:
165 # Add the dependency to the npm cache
166 destdir = os.path.join(d.getVar("S"), destsuffix)
167 tarball = npm_pack(env, destdir, tmpdir)
168 _npm_cache_add(tarball)
169 # Add its signature to the cached shrinkwrap
170 dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
171 dep["version"] = _npm_version(tarball)
172 dep["integrity"] = _npm_integrity(tarball)
173 if params.get("dev", False):
174 dep["dev"] = True
175 # Display progress
176 nonlocal progress_done
177 progress_done += 1
178 progress.write("%d/%d" % (progress_done, progress_total))
179
180 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600181
182 if has_shrinkwrap_file:
183 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
184 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500185
186 # Configure the main package
187 with tempfile.TemporaryDirectory() as tmpdir:
188 tarball = npm_pack(env, d.getVar("S"), tmpdir)
189 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
190
191 # Configure the cached manifest file and cached shrinkwrap file
192 def _update_manifest(depkey):
193 for name in orig_manifest.get(depkey, {}):
194 version = cached_shrinkwrap["dependencies"][name]["version"]
195 if depkey not in cached_manifest:
196 cached_manifest[depkey] = {}
197 cached_manifest[depkey][name] = version
198
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600199 if has_shrinkwrap_file:
200 _update_manifest("dependencies")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500201
202 if dev:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600203 if has_shrinkwrap_file:
204 _update_manifest("devDependencies")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500205
Andrew Geisslerd5838332022-05-27 11:33:10 -0500206 os.chmod(cached_manifest_file, os.stat(cached_manifest_file).st_mode | stat.S_IWUSR)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500207 with open(cached_manifest_file, "w") as f:
208 json.dump(cached_manifest, f, indent=2)
209
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600210 if has_shrinkwrap_file:
211 with open(cached_shrinkwrap_file, "w") as f:
212 json.dump(cached_shrinkwrap, f, indent=2)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500213}
214
215python npm_do_compile() {
216 """
217 Step two: install the npm package
218
219 Use the configured main package and the cached dependencies to run the
220 installation process. The installation is done in a directory which is
221 not the destination directory yet.
222
223 A combination of 'npm pack' and 'npm install' is used to ensure that the
224 installed files are actual copies instead of symbolic links (which is the
225 default npm behavior).
226 """
227 import shlex
228 import tempfile
229 from bb.fetch2.npm import NpmEnvironment
230
231 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
232
Andrew Geissler82c905d2020-04-13 13:39:40 -0500233 with tempfile.TemporaryDirectory() as tmpdir:
234 args = []
Andrew Geisslereff27472021-10-29 15:35:00 -0500235 configs = npm_global_configs(d)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500236
Andrew Geisslereff27472021-10-29 15:35:00 -0500237 if bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500238 configs.append(("also", "development"))
239 else:
240 configs.append(("only", "production"))
241
242 # Report as many logs as possible for debugging purpose
243 configs.append(("loglevel", "silly"))
244
245 # Configure the installation to be done globally in the build directory
246 configs.append(("global", "true"))
247 configs.append(("prefix", d.getVar("NPM_BUILD")))
248
249 # Add node-gyp configuration
250 configs.append(("arch", d.getVar("NPM_ARCH")))
251 configs.append(("release", "true"))
Andrew Geisslereff27472021-10-29 15:35:00 -0500252 configs.append(("nodedir", d.getVar("NPM_NODEDIR")))
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600253 configs.append(("python", d.getVar("PYTHON")))
Andrew Geissler82c905d2020-04-13 13:39:40 -0500254
Andrew Geisslereff27472021-10-29 15:35:00 -0500255 env = NpmEnvironment(d, configs)
256
Andrew Geissler82c905d2020-04-13 13:39:40 -0500257 # 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)
Andrew Geisslereff27472021-10-29 15:35:00 -0500263 cmd = "npm install %s %s" % (shlex.quote(tarball), d.getVar("EXTRA_OENPM"))
264 env.run(cmd, args=args)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500265}
266
267npm_do_install() {
Andrew Geissler82c905d2020-04-13 13:39:40 -0500268 # Step three: final install
269 #
270 # The previous installation have to be filtered to remove some extra files.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500271
Andrew Geissler82c905d2020-04-13 13:39:40 -0500272 rm -rf ${D}
273
274 # Copy the entire lib and bin directories
275 install -d ${D}/${nonarch_libdir}
276 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
277
278 if [ -d "${NPM_BUILD}/bin" ]
279 then
280 install -d ${D}/${bindir}
281 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
282 fi
283
284 # If the package (or its dependencies) uses node-gyp to build native addons,
285 # object files, static libraries or other temporary files can be hidden in
286 # the lib directory. To reduce the package size and to avoid QA issues
287 # (staticdev with static library files) these files must be removed.
288 local GYP_REGEX=".*/build/Release/[^/]*.node"
289
290 # Remove any node-gyp directory in ${D} to remove temporary build files
291 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
292 do
293 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
294
295 rm --recursive --force ${GYP_D_DIR}
296 done
297
298 # Copy only the node-gyp release files
299 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
300 do
301 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
302
303 install -d ${GYP_D_FILE%/*}
304 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
305 done
306
307 # Remove the shrinkwrap file which does not need to be packed
308 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
309 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500310}
311
Patrick Williams213cb262021-08-07 19:21:33 -0500312FILES:${PN} += " \
Brad Bishop15ae2502019-06-18 21:44:24 -0400313 ${bindir} \
Andrew Geissler82c905d2020-04-13 13:39:40 -0500314 ${nonarch_libdir} \
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500315"
316
Andrew Geissler82c905d2020-04-13 13:39:40 -0500317EXPORT_FUNCTIONS do_configure do_compile do_install