summaryrefslogtreecommitdiffstats
path: root/meta
diff options
context:
space:
mode:
authorJean-Marie LEMETAYER <jean-marie.lemetayer@savoirfairelinux.com>2020-01-24 18:07:31 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2020-01-27 16:48:09 +0000
commitde39f14d24a81d1f042cbcc4be6739e3e8000369 (patch)
tree443690fd4db422c178d2e079b8c1338a4ecaa255 /meta
parent97c882303e444d994c1f5be8c4d9a9472496d3ef (diff)
downloadpoky-de39f14d24a81d1f042cbcc4be6739e3e8000369.tar.gz
classes/npm: refactor the npm class
This commit splits the npm build in three steps: 1. With the new npmsw fetcher, the sources and dependencies of the package have been fetched and unpacked. As sources can also be patched, a local cache must be configured to use these modified sources. 2. Next, the installation process is run using the local cache only. Some packages may need to be compiled. 3. The final installation filters the previously installed files to avoid unnecessary files. This new version also fixes multiple issues related to npm dependencies badly handled: package names, scope packages, installation directories (From OE-Core rev: fb2252ee0777c6d26dea94c7588c323a6b97e961) Signed-off-by: Jean-Marie LEMETAYER <jean-marie.lemetayer@savoirfairelinux.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta')
-rw-r--r--meta/classes/npm.bbclass349
1 files changed, 274 insertions, 75 deletions
diff --git a/meta/classes/npm.bbclass b/meta/classes/npm.bbclass
index 4b1f0a39f0..799b2a3829 100644
--- a/meta/classes/npm.bbclass
+++ b/meta/classes/npm.bbclass
@@ -1,94 +1,293 @@
1# 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
1DEPENDS_prepend = "nodejs-native " 20DEPENDS_prepend = "nodejs-native "
2RDEPENDS_${PN}_prepend = "nodejs " 21RDEPENDS_${PN}_prepend = "nodejs "
3S = "${WORKDIR}/npmpkg"
4 22
5def node_pkgname(d): 23NPM_INSTALL_DEV ?= "0"
6 bpn = d.getVar('BPN') 24
7 if bpn.startswith("node-"): 25def npm_target_arch_map(target_arch):
8 return bpn[5:] 26 """Maps arch names to npm arch names"""
9 return bpn 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"
10 43
11NPMPN ?= "${@node_pkgname(d)}" 44def npm_global_configs(d):
45 """Get the npm global configuration"""
46 configs = []
47 # Configure the cache directory
48 configs.append(("cache", d.getVar("NPM_CACHE")))
49 return configs
12 50
13NPM_INSTALLDIR = "${libdir}/node_modules/${NPMPN}" 51def npm_pack(env, srcdir, workdir):
52 """Run 'npm pack' on a specified directory"""
53 import shlex
54 cmd = "npm pack %s" % shlex.quote(srcdir)
55 configs = [("ignore-scripts", "true")]
56 tarball = env.run(cmd, configs=configs, workdir=workdir).strip("\n")
57 return os.path.join(workdir, tarball)
14 58
15# function maps arch names to npm arch names 59python npm_do_configure() {
16def npm_oe_arch_map(target_arch, d): 60 """
61 Step one: configure the npm cache and the main npm package
62
63 Every dependencies have been fetched and patched in the source directory.
64 They have to be packed (this remove unneeded files) and added to the npm
65 cache to be available for the next step.
66
67 The main package and its associated manifest file and shrinkwrap file have
68 to be configured to take into account these cached dependencies.
69 """
70 import base64
71 import copy
72 import json
17 import re 73 import re
18 if re.match('p(pc|owerpc)(|64)', target_arch): return 'ppc' 74 import shlex
19 elif re.match('i.86$', target_arch): return 'ia32' 75 import tempfile
20 elif re.match('x86_64$', target_arch): return 'x64' 76 from bb.fetch2.npm import NpmEnvironment
21 elif re.match('arm64$', target_arch): return 'arm' 77 from bb.fetch2.npm import npm_unpack
22 return target_arch 78 from bb.fetch2.npmsw import foreach_dependencies
79 from bb.progress import OutOfProgressHandler
23 80
24NPM_ARCH ?= "${@npm_oe_arch_map(d.getVar('TARGET_ARCH'), d)}" 81 bb.utils.remove(d.getVar("NPM_CACHE"), recurse=True)
25NPM_INSTALL_DEV ?= "0" 82 bb.utils.remove(d.getVar("NPM_PACKAGE"), recurse=True)
83
84 env = NpmEnvironment(d, configs=npm_global_configs(d))
85
86 def _npm_cache_add(tarball):
87 """Run 'npm cache add' for a specified tarball"""
88 cmd = "npm cache add %s" % shlex.quote(tarball)
89 env.run(cmd)
90
91 def _npm_integrity(tarball):
92 """Return the npm integrity of a specified tarball"""
93 sha512 = bb.utils.sha512_file(tarball)
94 return "sha512-" + base64.b64encode(bytes.fromhex(sha512)).decode()
95
96 def _npm_version(tarball):
97 """Return the version of a specified tarball"""
98 regex = r"-(\d+\.\d+\.\d+(-.*)?(\+.*)?)\.tgz"
99 return re.search(regex, tarball).group(1)
100
101 def _npmsw_dependency_dict(orig, deptree):
102 """
103 Return the sub dictionary in the 'orig' dictionary corresponding to the
104 'deptree' dependency tree. This function follows the shrinkwrap file
105 format.
106 """
107 ptr = orig
108 for dep in deptree:
109 if "dependencies" not in ptr:
110 ptr["dependencies"] = {}
111 ptr = ptr["dependencies"]
112 if dep not in ptr:
113 ptr[dep] = {}
114 ptr = ptr[dep]
115 return ptr
116
117 # Manage the manifest file and shrinkwrap files
118 orig_manifest_file = d.expand("${S}/package.json")
119 orig_shrinkwrap_file = d.expand("${S}/npm-shrinkwrap.json")
120 cached_manifest_file = d.expand("${NPM_PACKAGE}/package.json")
121 cached_shrinkwrap_file = d.expand("${NPM_PACKAGE}/npm-shrinkwrap.json")
122
123 with open(orig_manifest_file, "r") as f:
124 orig_manifest = json.load(f)
125
126 cached_manifest = copy.deepcopy(orig_manifest)
127 cached_manifest.pop("dependencies", None)
128 cached_manifest.pop("devDependencies", None)
129
130 with open(orig_shrinkwrap_file, "r") as f:
131 orig_shrinkwrap = json.load(f)
132
133 cached_shrinkwrap = copy.deepcopy(orig_shrinkwrap)
134 cached_shrinkwrap.pop("dependencies", None)
135
136 # Manage the dependencies
137 progress = OutOfProgressHandler(d, r"^(\d+)/(\d+)$")
138 progress_total = 1 # also count the main package
139 progress_done = 0
140
141 def _count_dependency(name, params, deptree):
142 nonlocal progress_total
143 progress_total += 1
144
145 def _cache_dependency(name, params, deptree):
146 destsubdirs = [os.path.join("node_modules", dep) for dep in deptree]
147 destsuffix = os.path.join(*destsubdirs)
148 with tempfile.TemporaryDirectory() as tmpdir:
149 # Add the dependency to the npm cache
150 destdir = os.path.join(d.getVar("S"), destsuffix)
151 tarball = npm_pack(env, destdir, tmpdir)
152 _npm_cache_add(tarball)
153 # Add its signature to the cached shrinkwrap
154 dep = _npmsw_dependency_dict(cached_shrinkwrap, deptree)
155 dep["version"] = _npm_version(tarball)
156 dep["integrity"] = _npm_integrity(tarball)
157 if params.get("dev", False):
158 dep["dev"] = True
159 # Display progress
160 nonlocal progress_done
161 progress_done += 1
162 progress.write("%d/%d" % (progress_done, progress_total))
163
164 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
165 foreach_dependencies(orig_shrinkwrap, _count_dependency, dev)
166 foreach_dependencies(orig_shrinkwrap, _cache_dependency, dev)
167
168 # Configure the main package
169 with tempfile.TemporaryDirectory() as tmpdir:
170 tarball = npm_pack(env, d.getVar("S"), tmpdir)
171 npm_unpack(tarball, d.getVar("NPM_PACKAGE"), d)
172
173 # Configure the cached manifest file and cached shrinkwrap file
174 def _update_manifest(depkey):
175 for name in orig_manifest.get(depkey, {}):
176 version = cached_shrinkwrap["dependencies"][name]["version"]
177 if depkey not in cached_manifest:
178 cached_manifest[depkey] = {}
179 cached_manifest[depkey][name] = version
26 180
27npm_do_compile() { 181 _update_manifest("dependencies")
28 # Copy in any additionally fetched modules 182
29 if [ -d ${WORKDIR}/node_modules ] ; then 183 if dev:
30 cp -a ${WORKDIR}/node_modules ${S}/ 184 _update_manifest("devDependencies")
31 fi 185
32 # changing the home directory to the working directory, the .npmrc will 186 with open(cached_manifest_file, "w") as f:
33 # be created in this directory 187 json.dump(cached_manifest, f, indent=2)
34 export HOME=${WORKDIR} 188
35 if [ "${NPM_INSTALL_DEV}" = "1" ]; then 189 with open(cached_shrinkwrap_file, "w") as f:
36 npm config set dev true 190 json.dump(cached_shrinkwrap, f, indent=2)
37 else
38 npm config set dev false
39 fi
40 npm set cache ${WORKDIR}/npm_cache
41 # clear cache before every build
42 npm cache clear --force
43 # Install pkg into ${S} without going to the registry
44 if [ "${NPM_INSTALL_DEV}" = "1" ]; then
45 npm --arch=${NPM_ARCH} --target_arch=${NPM_ARCH} --no-registry install
46 else
47 npm --arch=${NPM_ARCH} --target_arch=${NPM_ARCH} --production --no-registry install
48 fi
49} 191}
50 192
51npm_do_install() { 193python npm_do_compile() {
52 # changing the home directory to the working directory, the .npmrc will 194 """
53 # be created in this directory 195 Step two: install the npm package
54 export HOME=${WORKDIR} 196
55 mkdir -p ${D}${libdir}/node_modules 197 Use the configured main package and the cached dependencies to run the
56 local NPM_PACKFILE=$(npm pack .) 198 installation process. The installation is done in a directory which is
57 npm install --prefix ${D}${prefix} -g --arch=${NPM_ARCH} --target_arch=${NPM_ARCH} --production --no-registry ${NPM_PACKFILE} 199 not the destination directory yet.
58 ln -fs node_modules ${D}${libdir}/node 200
59 find ${D}${NPM_INSTALLDIR} -type f \( -name "*.a" -o -name "*.d" -o -name "*.o" \) -delete 201 A combination of 'npm pack' and 'npm install' is used to ensure that the
60 if [ -d ${D}${prefix}/etc ] ; then 202 installed files are actual copies instead of symbolic links (which is the
61 # This will be empty 203 default npm behavior).
62 rmdir ${D}${prefix}/etc 204 """
63 fi 205 import shlex
206 import tempfile
207 from bb.fetch2.npm import NpmEnvironment
208
209 bb.utils.remove(d.getVar("NPM_BUILD"), recurse=True)
210
211 env = NpmEnvironment(d, configs=npm_global_configs(d))
212
213 dev = bb.utils.to_boolean(d.getVar("NPM_INSTALL_DEV"), False)
214
215 with tempfile.TemporaryDirectory() as tmpdir:
216 configs = []
217
218 if dev:
219 configs.append(("also", "development"))
220 else:
221 configs.append(("only", "production"))
222
223 # Report as many logs as possible for debugging purpose
224 configs.append(("loglevel", "silly"))
225
226 # Configure the installation to be done globally in the build directory
227 configs.append(("global", "true"))
228 configs.append(("prefix", d.getVar("NPM_BUILD")))
229
230 # Add node-gyp configuration
231 configs.append(("arch", d.getVar("NPM_ARCH")))
232 configs.append(("release", "true"))
233
234 # Pack and install the main package
235 tarball = npm_pack(env, d.getVar("NPM_PACKAGE"), tmpdir)
236 env.run("npm install %s" % shlex.quote(tarball), configs=configs)
64} 237}
65 238
66python populate_packages_prepend () { 239npm_do_install() {
67 instdir = d.expand('${D}${NPM_INSTALLDIR}') 240 # Step three: final install
68 extrapackages = oe.package.npm_split_package_dirs(instdir) 241 #
69 pkgnames = extrapackages.keys() 242 # The previous installation have to be filtered to remove some extra files.
70 d.prependVar('PACKAGES', '%s ' % ' '.join(pkgnames)) 243
71 for pkgname in pkgnames: 244 rm -rf ${D}
72 pkgrelpath, pdata = extrapackages[pkgname] 245
73 pkgpath = '${NPM_INSTALLDIR}/' + pkgrelpath 246 # Copy the entire lib and bin directories
74 # package names can't have underscores but npm packages sometimes use them 247 install -d ${D}/${nonarch_libdir}
75 oe_pkg_name = pkgname.replace('_', '-') 248 cp --no-preserve=ownership --recursive ${NPM_BUILD}/lib/. ${D}/${nonarch_libdir}
76 expanded_pkgname = d.expand(oe_pkg_name) 249
77 d.setVar('FILES_%s' % expanded_pkgname, pkgpath) 250 if [ -d "${NPM_BUILD}/bin" ]
78 if pdata: 251 then
79 version = pdata.get('version', None) 252 install -d ${D}/${bindir}
80 if version: 253 cp --no-preserve=ownership --recursive ${NPM_BUILD}/bin/. ${D}/${bindir}
81 d.setVar('PKGV_%s' % expanded_pkgname, version) 254 fi
82 description = pdata.get('description', None) 255
83 if description: 256 # If the package (or its dependencies) uses node-gyp to build native addons,
84 d.setVar('SUMMARY_%s' % expanded_pkgname, description.replace(u"\u2018", "'").replace(u"\u2019", "'")) 257 # object files, static libraries or other temporary files can be hidden in
85 d.appendVar('RDEPENDS_%s' % d.getVar('PN'), ' %s' % ' '.join(pkgnames).replace('_', '-')) 258 # the lib directory. To reduce the package size and to avoid QA issues
259 # (staticdev with static library files) these files must be removed.
260 local GYP_REGEX=".*/build/Release/[^/]*.node"
261
262 # Remove any node-gyp directory in ${D} to remove temporary build files
263 for GYP_D_FILE in $(find ${D} -regex "${GYP_REGEX}")
264 do
265 local GYP_D_DIR=${GYP_D_FILE%/Release/*}
266
267 rm --recursive --force ${GYP_D_DIR}
268 done
269
270 # Copy only the node-gyp release files
271 for GYP_B_FILE in $(find ${NPM_BUILD} -regex "${GYP_REGEX}")
272 do
273 local GYP_D_FILE=${D}/${prefix}/${GYP_B_FILE#${NPM_BUILD}}
274
275 install -d ${GYP_D_FILE%/*}
276 install -m 755 ${GYP_B_FILE} ${GYP_D_FILE}
277 done
278
279 # Remove the shrinkwrap file which does not need to be packed
280 rm -f ${D}/${nonarch_libdir}/node_modules/*/npm-shrinkwrap.json
281 rm -f ${D}/${nonarch_libdir}/node_modules/@*/*/npm-shrinkwrap.json
282
283 # node(1) is using /usr/lib/node as default include directory and npm(1) is
284 # using /usr/lib/node_modules as install directory. Let's make both happy.
285 ln -fs node_modules ${D}/${nonarch_libdir}/node
86} 286}
87 287
88FILES_${PN} += " \ 288FILES_${PN} += " \
89 ${bindir} \ 289 ${bindir} \
90 ${libdir}/node \ 290 ${nonarch_libdir} \
91 ${NPM_INSTALLDIR} \
92" 291"
93 292
94EXPORT_FUNCTIONS do_compile do_install 293EXPORT_FUNCTIONS do_configure do_compile do_install