| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame^] | 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 |  | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 20 | DEPENDS_prepend = "nodejs-native " | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 21 | RDEPENDS_${PN}_prepend = "nodejs " | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 22 |  | 
| Brad Bishop | 1a4b7ee | 2018-12-16 17:11:34 -0800 | [diff] [blame] | 23 | NPM_INSTALL_DEV ?= "0" | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 24 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame^] | 25 | def 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 |  | 
 | 38 | NPM_ARCH ?= "${@npm_target_arch_map(d.getVar("TARGET_ARCH"))}" | 
 | 39 |  | 
 | 40 | NPM_PACKAGE = "${WORKDIR}/npm-package" | 
 | 41 | NPM_CACHE = "${WORKDIR}/npm-cache" | 
 | 42 | NPM_BUILD = "${WORKDIR}/npm-build" | 
 | 43 |  | 
 | 44 | def 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 |  | 
 | 54 | def 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 |  | 
 | 62 | python 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 |  | 
 | 196 | python 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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 251 | } | 
 | 252 |  | 
 | 253 | npm_do_install() { | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame^] | 254 |     # Step three: final install | 
 | 255 |     # | 
 | 256 |     # The previous installation have to be filtered to remove some extra files. | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 257 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame^] | 258 |     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 Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 300 | } | 
 | 301 |  | 
 | 302 | FILES_${PN} += " \ | 
| Brad Bishop | 15ae250 | 2019-06-18 21:44:24 -0400 | [diff] [blame] | 303 |     ${bindir} \ | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame^] | 304 |     ${nonarch_libdir} \ | 
| Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 305 | " | 
 | 306 |  | 
| Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame^] | 307 | EXPORT_FUNCTIONS do_configure do_compile do_install |