blob: 113a89f6a6f932d82668b53a0559a223167cc079 [file] [log] [blame]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001# Copyright (C) 2016 Intel Corporation
Andrew Geissler82c905d2020-04-13 13:39:40 -05002# Copyright (C) 2020 Savoir-Faire Linux
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05003#
Brad Bishopc342db32019-05-15 21:57:59 -04004# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05005#
Andrew Geissler82c905d2020-04-13 13:39:40 -05006"""Recipe creation tool - npm module support plugin"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05007
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05008import json
Andrew Geisslereff27472021-10-29 15:35:00 -05009import logging
Andrew Geissler82c905d2020-04-13 13:39:40 -050010import os
11import re
12import sys
13import tempfile
14import bb
15from bb.fetch2.npm import NpmEnvironment
Andrew Geissler8f840682023-07-21 09:09:43 -050016from bb.fetch2.npm import npm_package
Andrew Geissler82c905d2020-04-13 13:39:40 -050017from bb.fetch2.npmsw import foreach_dependencies
18from recipetool.create import RecipeHandler
Andrew Geisslereff27472021-10-29 15:35:00 -050019from recipetool.create import get_license_md5sums
Andrew Geissler82c905d2020-04-13 13:39:40 -050020from recipetool.create import guess_license
21from recipetool.create import split_pkg_licenses
Andrew Geisslereff27472021-10-29 15:35:00 -050022logger = logging.getLogger('recipetool')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050023
Andrew Geissler82c905d2020-04-13 13:39:40 -050024TINFOIL = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050025
26def tinfoil_init(instance):
Andrew Geissler82c905d2020-04-13 13:39:40 -050027 """Initialize tinfoil"""
28 global TINFOIL
29 TINFOIL = instance
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050030
31class NpmRecipeHandler(RecipeHandler):
Andrew Geissler82c905d2020-04-13 13:39:40 -050032 """Class to handle the npm recipe creation"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050033
Andrew Geissler82c905d2020-04-13 13:39:40 -050034 @staticmethod
Andrew Geissler82c905d2020-04-13 13:39:40 -050035 def _get_registry(lines):
36 """Get the registry value from the 'npm://registry' url"""
37 registry = None
38
39 def _handle_registry(varname, origvalue, op, newlines):
40 nonlocal registry
41 if origvalue.startswith("npm://"):
42 registry = re.sub(r"^npm://", "http://", origvalue.split(";")[0])
43 return origvalue, None, 0, True
44
45 bb.utils.edit_metadata(lines, ["SRC_URI"], _handle_registry)
46
47 return registry
48
49 @staticmethod
50 def _ensure_npm():
51 """Check if the 'npm' command is available in the recipes"""
52 if not TINFOIL.recipes_parsed:
53 TINFOIL.parse_recipes()
54
Brad Bishopd7bf8c12018-02-25 22:55:05 -050055 try:
Andrew Geissler82c905d2020-04-13 13:39:40 -050056 d = TINFOIL.parse_recipe("nodejs-native")
Brad Bishopd7bf8c12018-02-25 22:55:05 -050057 except bb.providers.NoProvider:
Andrew Geissler82c905d2020-04-13 13:39:40 -050058 bb.error("Nothing provides 'nodejs-native' which is required for the build")
59 bb.note("You will likely need to add a layer that provides nodejs")
60 sys.exit(14)
61
62 bindir = d.getVar("STAGING_BINDIR_NATIVE")
63 npmpath = os.path.join(bindir, "npm")
64
Brad Bishopd7bf8c12018-02-25 22:55:05 -050065 if not os.path.exists(npmpath):
Andrew Geissler82c905d2020-04-13 13:39:40 -050066 TINFOIL.build_targets("nodejs-native", "addto_recipe_sysroot")
67
Brad Bishopd7bf8c12018-02-25 22:55:05 -050068 if not os.path.exists(npmpath):
Andrew Geissler82c905d2020-04-13 13:39:40 -050069 bb.error("Failed to add 'npm' to sysroot")
70 sys.exit(14)
71
Brad Bishopd7bf8c12018-02-25 22:55:05 -050072 return bindir
73
Andrew Geissler82c905d2020-04-13 13:39:40 -050074 @staticmethod
75 def _npm_global_configs(dev):
76 """Get the npm global configuration"""
77 configs = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050078
Andrew Geissler82c905d2020-04-13 13:39:40 -050079 if dev:
80 configs.append(("also", "development"))
81 else:
82 configs.append(("only", "production"))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050083
Andrew Geissler82c905d2020-04-13 13:39:40 -050084 configs.append(("save", "false"))
85 configs.append(("package-lock", "false"))
86 configs.append(("shrinkwrap", "false"))
87 return configs
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050088
Andrew Geissler82c905d2020-04-13 13:39:40 -050089 def _run_npm_install(self, d, srctree, registry, dev):
90 """Run the 'npm install' command without building the addons"""
91 configs = self._npm_global_configs(dev)
92 configs.append(("ignore-scripts", "true"))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050093
Andrew Geissler82c905d2020-04-13 13:39:40 -050094 if registry:
95 configs.append(("registry", registry))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050096
Andrew Geissler82c905d2020-04-13 13:39:40 -050097 bb.utils.remove(os.path.join(srctree, "node_modules"), recurse=True)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050098
Andrew Geissler82c905d2020-04-13 13:39:40 -050099 env = NpmEnvironment(d, configs=configs)
100 env.run("npm install", workdir=srctree)
101
102 def _generate_shrinkwrap(self, d, srctree, dev):
103 """Check and generate the 'npm-shrinkwrap.json' file if needed"""
104 configs = self._npm_global_configs(dev)
105
106 env = NpmEnvironment(d, configs=configs)
107 env.run("npm shrinkwrap", workdir=srctree)
108
109 return os.path.join(srctree, "npm-shrinkwrap.json")
110
111 def _handle_licenses(self, srctree, shrinkwrap_file, dev):
112 """Return the extra license files and the list of packages"""
113 licfiles = []
114 packages = {}
115
Andrew Geissler82c905d2020-04-13 13:39:40 -0500116 # Handle the parent package
Andrew Geissler82c905d2020-04-13 13:39:40 -0500117 packages["${PN}"] = ""
118
Andrew Geisslereff27472021-10-29 15:35:00 -0500119 def _licfiles_append_fallback_readme_files(destdir):
120 """Append README files as fallback to license files if a license files is missing"""
121
122 fallback = True
123 readmes = []
124 basedir = os.path.join(srctree, destdir)
125 for fn in os.listdir(basedir):
126 upper = fn.upper()
127 if upper.startswith("README"):
128 fullpath = os.path.join(basedir, fn)
129 readmes.append(fullpath)
130 if upper.startswith("COPYING") or "LICENCE" in upper or "LICENSE" in upper:
131 fallback = False
132 if fallback:
133 for readme in readmes:
134 licfiles.append(os.path.relpath(readme, srctree))
135
Andrew Geissler82c905d2020-04-13 13:39:40 -0500136 # Handle the dependencies
Andrew Geissler8f840682023-07-21 09:09:43 -0500137 def _handle_dependency(name, params, destdir):
138 deptree = destdir.split('node_modules/')
139 suffix = "-".join([npm_package(dep) for dep in deptree])
140 packages["${PN}" + suffix] = destdir
Andrew Geisslereff27472021-10-29 15:35:00 -0500141 _licfiles_append_fallback_readme_files(destdir)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500142
143 with open(shrinkwrap_file, "r") as f:
144 shrinkwrap = json.load(f)
145
146 foreach_dependencies(shrinkwrap, _handle_dependency, dev)
147
148 return licfiles, packages
Andrew Geissler8f840682023-07-21 09:09:43 -0500149
150 # Handle the peer dependencies
151 def _handle_peer_dependency(self, shrinkwrap_file):
152 """Check if package has peer dependencies and show warning if it is the case"""
153 with open(shrinkwrap_file, "r") as f:
154 shrinkwrap = json.load(f)
155
156 packages = shrinkwrap.get("packages", {})
157 peer_deps = packages.get("", {}).get("peerDependencies", {})
158
159 for peer_dep in peer_deps:
160 peer_dep_yocto_name = npm_package(peer_dep)
161 bb.warn(peer_dep + " is a peer dependencie of the actual package. " +
162 "Please add this peer dependencie to the RDEPENDS variable as %s and generate its recipe with devtool"
163 % peer_dep_yocto_name)
164
165
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600166
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500167 def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500168 """Handle the npm recipe creation"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500169
Andrew Geissler82c905d2020-04-13 13:39:40 -0500170 if "buildsystem" in handled:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500171 return False
172
Andrew Geissler82c905d2020-04-13 13:39:40 -0500173 files = RecipeHandler.checkfiles(srctree, ["package.json"])
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500174
Andrew Geissler82c905d2020-04-13 13:39:40 -0500175 if not files:
176 return False
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600177
Andrew Geissler82c905d2020-04-13 13:39:40 -0500178 with open(files[0], "r") as f:
179 data = json.load(f)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600180
Andrew Geissler82c905d2020-04-13 13:39:40 -0500181 if "name" not in data or "version" not in data:
182 return False
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500183
Andrew Geissler8f840682023-07-21 09:09:43 -0500184 extravalues["PN"] = npm_package(data["name"])
Andrew Geissler82c905d2020-04-13 13:39:40 -0500185 extravalues["PV"] = data["version"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500186
Andrew Geissler82c905d2020-04-13 13:39:40 -0500187 if "description" in data:
188 extravalues["SUMMARY"] = data["description"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500189
Andrew Geissler82c905d2020-04-13 13:39:40 -0500190 if "homepage" in data:
191 extravalues["HOMEPAGE"] = data["homepage"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500192
Andrew Geissler82c905d2020-04-13 13:39:40 -0500193 dev = bb.utils.to_boolean(str(extravalues.get("NPM_INSTALL_DEV", "0")), False)
194 registry = self._get_registry(lines_before)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600195
Andrew Geissler82c905d2020-04-13 13:39:40 -0500196 bb.note("Checking if npm is available ...")
197 # The native npm is used here (and not the host one) to ensure that the
198 # npm version is high enough to ensure an efficient dependency tree
199 # resolution and avoid issue with the shrinkwrap file format.
200 # Moreover the native npm is mandatory for the build.
201 bindir = self._ensure_npm()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500202
Andrew Geissler82c905d2020-04-13 13:39:40 -0500203 d = bb.data.createCopy(TINFOIL.config_data)
204 d.prependVar("PATH", bindir + ":")
205 d.setVar("S", srctree)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500206
Andrew Geissler82c905d2020-04-13 13:39:40 -0500207 bb.note("Generating shrinkwrap file ...")
208 # To generate the shrinkwrap file the dependencies have to be installed
209 # first. During the generation process some files may be updated /
210 # deleted. By default devtool tracks the diffs in the srctree and raises
211 # errors when finishing the recipe if some diffs are found.
212 git_exclude_file = os.path.join(srctree, ".git", "info", "exclude")
213 if os.path.exists(git_exclude_file):
214 with open(git_exclude_file, "r+") as f:
215 lines = f.readlines()
216 for line in ["/node_modules/", "/npm-shrinkwrap.json"]:
217 if line not in lines:
218 f.write(line + "\n")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600219
Andrew Geissler82c905d2020-04-13 13:39:40 -0500220 lock_file = os.path.join(srctree, "package-lock.json")
221 lock_copy = lock_file + ".copy"
222 if os.path.exists(lock_file):
223 bb.utils.copyfile(lock_file, lock_copy)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500224
Andrew Geissler82c905d2020-04-13 13:39:40 -0500225 self._run_npm_install(d, srctree, registry, dev)
226 shrinkwrap_file = self._generate_shrinkwrap(d, srctree, dev)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500227
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600228 with open(shrinkwrap_file, "r") as f:
229 shrinkwrap = json.load(f)
230
Andrew Geissler82c905d2020-04-13 13:39:40 -0500231 if os.path.exists(lock_copy):
232 bb.utils.movefile(lock_copy, lock_file)
233
234 # Add the shrinkwrap file as 'extrafiles'
235 shrinkwrap_copy = shrinkwrap_file + ".copy"
236 bb.utils.copyfile(shrinkwrap_file, shrinkwrap_copy)
237 extravalues.setdefault("extrafiles", {})
238 extravalues["extrafiles"]["npm-shrinkwrap.json"] = shrinkwrap_copy
239
240 url_local = "npmsw://%s" % shrinkwrap_file
241 url_recipe= "npmsw://${THISDIR}/${BPN}/npm-shrinkwrap.json"
242
243 if dev:
244 url_local += ";dev=1"
245 url_recipe += ";dev=1"
246
247 # Add the npmsw url in the SRC_URI of the generated recipe
248 def _handle_srcuri(varname, origvalue, op, newlines):
249 """Update the version value and add the 'npmsw://' url"""
250 value = origvalue.replace("version=" + data["version"], "version=${PV}")
251 value = value.replace("version=latest", "version=${PV}")
252 values = [line.strip() for line in value.strip('\n').splitlines()]
Andrew Geissler8f840682023-07-21 09:09:43 -0500253 if "dependencies" in shrinkwrap.get("packages", {}).get("", {}):
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600254 values.append(url_recipe)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500255 return values, None, 4, False
256
257 (_, newlines) = bb.utils.edit_metadata(lines_before, ["SRC_URI"], _handle_srcuri)
258 lines_before[:] = [line.rstrip('\n') for line in newlines]
259
260 # In order to generate correct licence checksums in the recipe the
261 # dependencies have to be fetched again using the npmsw url
262 bb.note("Fetching npm dependencies ...")
263 bb.utils.remove(os.path.join(srctree, "node_modules"), recurse=True)
264 fetcher = bb.fetch2.Fetch([url_local], d)
265 fetcher.download()
266 fetcher.unpack(srctree)
267
268 bb.note("Handling licences ...")
269 (licfiles, packages) = self._handle_licenses(srctree, shrinkwrap_file, dev)
Andrew Geisslereff27472021-10-29 15:35:00 -0500270
271 def _guess_odd_license(licfiles):
272 import bb
273
274 md5sums = get_license_md5sums(d, linenumbers=True)
275
276 chksums = []
277 licenses = []
278 for licfile in licfiles:
279 f = os.path.join(srctree, licfile)
280 md5value = bb.utils.md5_file(f)
281 (license, beginline, endline, md5) = md5sums.get(md5value,
282 (None, "", "", ""))
283 if not license:
284 license = "Unknown"
285 logger.info("Please add the following line for '%s' to a "
286 "'lib/recipetool/licenses.csv' and replace `Unknown`, "
287 "`X`, `Y` and `MD5` with the license, begin line, "
288 "end line and partial MD5 checksum:\n" \
289 "%s,Unknown,X,Y,MD5" % (licfile, md5value))
290 chksums.append("file://%s%s%s;md5=%s" % (licfile,
291 ";beginline=%s" % (beginline) if beginline else "",
292 ";endline=%s" % (endline) if endline else "",
293 md5 if md5 else md5value))
294 licenses.append((license, licfile, md5value))
295 return (licenses, chksums)
296
297 (licenses, extravalues["LIC_FILES_CHKSUM"]) = _guess_odd_license(licfiles)
298 split_pkg_licenses([*licenses, *guess_license(srctree, d)], packages, lines_after)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500299
300 classes.append("npm")
301 handled.append("buildsystem")
302
Andrew Geissler8f840682023-07-21 09:09:43 -0500303 # Check if package has peer dependencies and inform the user
304 self._handle_peer_dependency(shrinkwrap_file)
305
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500306 return True
307
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500308def register_recipe_handlers(handlers):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500309 """Register the npm handler"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500310 handlers.append((NpmRecipeHandler(), 60))