blob: 3394a899701efc7427ad717102de51a3afa0eb41 [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
16from bb.fetch2.npmsw import foreach_dependencies
17from recipetool.create import RecipeHandler
Andrew Geisslereff27472021-10-29 15:35:00 -050018from recipetool.create import get_license_md5sums
Andrew Geissler82c905d2020-04-13 13:39:40 -050019from recipetool.create import guess_license
20from recipetool.create import split_pkg_licenses
Andrew Geisslereff27472021-10-29 15:35:00 -050021logger = logging.getLogger('recipetool')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022
Andrew Geissler82c905d2020-04-13 13:39:40 -050023TINFOIL = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024
25def tinfoil_init(instance):
Andrew Geissler82c905d2020-04-13 13:39:40 -050026 """Initialize tinfoil"""
27 global TINFOIL
28 TINFOIL = instance
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050029
30class NpmRecipeHandler(RecipeHandler):
Andrew Geissler82c905d2020-04-13 13:39:40 -050031 """Class to handle the npm recipe creation"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050032
Andrew Geissler82c905d2020-04-13 13:39:40 -050033 @staticmethod
34 def _npm_name(name):
35 """Generate a Yocto friendly npm name"""
36 name = re.sub("/", "-", name)
37 name = name.lower()
38 name = re.sub(r"[^\-a-z0-9]", "", name)
39 name = name.strip("-")
40 return name
41
42 @staticmethod
43 def _get_registry(lines):
44 """Get the registry value from the 'npm://registry' url"""
45 registry = None
46
47 def _handle_registry(varname, origvalue, op, newlines):
48 nonlocal registry
49 if origvalue.startswith("npm://"):
50 registry = re.sub(r"^npm://", "http://", origvalue.split(";")[0])
51 return origvalue, None, 0, True
52
53 bb.utils.edit_metadata(lines, ["SRC_URI"], _handle_registry)
54
55 return registry
56
57 @staticmethod
58 def _ensure_npm():
59 """Check if the 'npm' command is available in the recipes"""
60 if not TINFOIL.recipes_parsed:
61 TINFOIL.parse_recipes()
62
Brad Bishopd7bf8c12018-02-25 22:55:05 -050063 try:
Andrew Geissler82c905d2020-04-13 13:39:40 -050064 d = TINFOIL.parse_recipe("nodejs-native")
Brad Bishopd7bf8c12018-02-25 22:55:05 -050065 except bb.providers.NoProvider:
Andrew Geissler82c905d2020-04-13 13:39:40 -050066 bb.error("Nothing provides 'nodejs-native' which is required for the build")
67 bb.note("You will likely need to add a layer that provides nodejs")
68 sys.exit(14)
69
70 bindir = d.getVar("STAGING_BINDIR_NATIVE")
71 npmpath = os.path.join(bindir, "npm")
72
Brad Bishopd7bf8c12018-02-25 22:55:05 -050073 if not os.path.exists(npmpath):
Andrew Geissler82c905d2020-04-13 13:39:40 -050074 TINFOIL.build_targets("nodejs-native", "addto_recipe_sysroot")
75
Brad Bishopd7bf8c12018-02-25 22:55:05 -050076 if not os.path.exists(npmpath):
Andrew Geissler82c905d2020-04-13 13:39:40 -050077 bb.error("Failed to add 'npm' to sysroot")
78 sys.exit(14)
79
Brad Bishopd7bf8c12018-02-25 22:55:05 -050080 return bindir
81
Andrew Geissler82c905d2020-04-13 13:39:40 -050082 @staticmethod
83 def _npm_global_configs(dev):
84 """Get the npm global configuration"""
85 configs = []
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050086
Andrew Geissler82c905d2020-04-13 13:39:40 -050087 if dev:
88 configs.append(("also", "development"))
89 else:
90 configs.append(("only", "production"))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050091
Andrew Geissler82c905d2020-04-13 13:39:40 -050092 configs.append(("save", "false"))
93 configs.append(("package-lock", "false"))
94 configs.append(("shrinkwrap", "false"))
95 return configs
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050096
Andrew Geissler82c905d2020-04-13 13:39:40 -050097 def _run_npm_install(self, d, srctree, registry, dev):
98 """Run the 'npm install' command without building the addons"""
99 configs = self._npm_global_configs(dev)
100 configs.append(("ignore-scripts", "true"))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500101
Andrew Geissler82c905d2020-04-13 13:39:40 -0500102 if registry:
103 configs.append(("registry", registry))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500104
Andrew Geissler82c905d2020-04-13 13:39:40 -0500105 bb.utils.remove(os.path.join(srctree, "node_modules"), recurse=True)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500106
Andrew Geissler82c905d2020-04-13 13:39:40 -0500107 env = NpmEnvironment(d, configs=configs)
108 env.run("npm install", workdir=srctree)
109
110 def _generate_shrinkwrap(self, d, srctree, dev):
111 """Check and generate the 'npm-shrinkwrap.json' file if needed"""
112 configs = self._npm_global_configs(dev)
113
114 env = NpmEnvironment(d, configs=configs)
115 env.run("npm shrinkwrap", workdir=srctree)
116
117 return os.path.join(srctree, "npm-shrinkwrap.json")
118
119 def _handle_licenses(self, srctree, shrinkwrap_file, dev):
120 """Return the extra license files and the list of packages"""
121 licfiles = []
122 packages = {}
123
Andrew Geissler82c905d2020-04-13 13:39:40 -0500124 # Handle the parent package
Andrew Geissler82c905d2020-04-13 13:39:40 -0500125 packages["${PN}"] = ""
126
Andrew Geisslereff27472021-10-29 15:35:00 -0500127 def _licfiles_append_fallback_readme_files(destdir):
128 """Append README files as fallback to license files if a license files is missing"""
129
130 fallback = True
131 readmes = []
132 basedir = os.path.join(srctree, destdir)
133 for fn in os.listdir(basedir):
134 upper = fn.upper()
135 if upper.startswith("README"):
136 fullpath = os.path.join(basedir, fn)
137 readmes.append(fullpath)
138 if upper.startswith("COPYING") or "LICENCE" in upper or "LICENSE" in upper:
139 fallback = False
140 if fallback:
141 for readme in readmes:
142 licfiles.append(os.path.relpath(readme, srctree))
143
Andrew Geissler82c905d2020-04-13 13:39:40 -0500144 # Handle the dependencies
145 def _handle_dependency(name, params, deptree):
146 suffix = "-".join([self._npm_name(dep) for dep in deptree])
147 destdirs = [os.path.join("node_modules", dep) for dep in deptree]
148 destdir = os.path.join(*destdirs)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500149 packages["${PN}-" + suffix] = destdir
Andrew Geisslereff27472021-10-29 15:35:00 -0500150 _licfiles_append_fallback_readme_files(destdir)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500151
152 with open(shrinkwrap_file, "r") as f:
153 shrinkwrap = json.load(f)
154
155 foreach_dependencies(shrinkwrap, _handle_dependency, dev)
156
157 return licfiles, packages
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600158
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500159 def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500160 """Handle the npm recipe creation"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500161
Andrew Geissler82c905d2020-04-13 13:39:40 -0500162 if "buildsystem" in handled:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500163 return False
164
Andrew Geissler82c905d2020-04-13 13:39:40 -0500165 files = RecipeHandler.checkfiles(srctree, ["package.json"])
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500166
Andrew Geissler82c905d2020-04-13 13:39:40 -0500167 if not files:
168 return False
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600169
Andrew Geissler82c905d2020-04-13 13:39:40 -0500170 with open(files[0], "r") as f:
171 data = json.load(f)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600172
Andrew Geissler82c905d2020-04-13 13:39:40 -0500173 if "name" not in data or "version" not in data:
174 return False
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500175
Andrew Geissler82c905d2020-04-13 13:39:40 -0500176 extravalues["PN"] = self._npm_name(data["name"])
177 extravalues["PV"] = data["version"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500178
Andrew Geissler82c905d2020-04-13 13:39:40 -0500179 if "description" in data:
180 extravalues["SUMMARY"] = data["description"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500181
Andrew Geissler82c905d2020-04-13 13:39:40 -0500182 if "homepage" in data:
183 extravalues["HOMEPAGE"] = data["homepage"]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500184
Andrew Geissler82c905d2020-04-13 13:39:40 -0500185 dev = bb.utils.to_boolean(str(extravalues.get("NPM_INSTALL_DEV", "0")), False)
186 registry = self._get_registry(lines_before)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600187
Andrew Geissler82c905d2020-04-13 13:39:40 -0500188 bb.note("Checking if npm is available ...")
189 # The native npm is used here (and not the host one) to ensure that the
190 # npm version is high enough to ensure an efficient dependency tree
191 # resolution and avoid issue with the shrinkwrap file format.
192 # Moreover the native npm is mandatory for the build.
193 bindir = self._ensure_npm()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500194
Andrew Geissler82c905d2020-04-13 13:39:40 -0500195 d = bb.data.createCopy(TINFOIL.config_data)
196 d.prependVar("PATH", bindir + ":")
197 d.setVar("S", srctree)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500198
Andrew Geissler82c905d2020-04-13 13:39:40 -0500199 bb.note("Generating shrinkwrap file ...")
200 # To generate the shrinkwrap file the dependencies have to be installed
201 # first. During the generation process some files may be updated /
202 # deleted. By default devtool tracks the diffs in the srctree and raises
203 # errors when finishing the recipe if some diffs are found.
204 git_exclude_file = os.path.join(srctree, ".git", "info", "exclude")
205 if os.path.exists(git_exclude_file):
206 with open(git_exclude_file, "r+") as f:
207 lines = f.readlines()
208 for line in ["/node_modules/", "/npm-shrinkwrap.json"]:
209 if line not in lines:
210 f.write(line + "\n")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600211
Andrew Geissler82c905d2020-04-13 13:39:40 -0500212 lock_file = os.path.join(srctree, "package-lock.json")
213 lock_copy = lock_file + ".copy"
214 if os.path.exists(lock_file):
215 bb.utils.copyfile(lock_file, lock_copy)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500216
Andrew Geissler82c905d2020-04-13 13:39:40 -0500217 self._run_npm_install(d, srctree, registry, dev)
218 shrinkwrap_file = self._generate_shrinkwrap(d, srctree, dev)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500219
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600220 with open(shrinkwrap_file, "r") as f:
221 shrinkwrap = json.load(f)
222
Andrew Geissler82c905d2020-04-13 13:39:40 -0500223 if os.path.exists(lock_copy):
224 bb.utils.movefile(lock_copy, lock_file)
225
226 # Add the shrinkwrap file as 'extrafiles'
227 shrinkwrap_copy = shrinkwrap_file + ".copy"
228 bb.utils.copyfile(shrinkwrap_file, shrinkwrap_copy)
229 extravalues.setdefault("extrafiles", {})
230 extravalues["extrafiles"]["npm-shrinkwrap.json"] = shrinkwrap_copy
231
232 url_local = "npmsw://%s" % shrinkwrap_file
233 url_recipe= "npmsw://${THISDIR}/${BPN}/npm-shrinkwrap.json"
234
235 if dev:
236 url_local += ";dev=1"
237 url_recipe += ";dev=1"
238
239 # Add the npmsw url in the SRC_URI of the generated recipe
240 def _handle_srcuri(varname, origvalue, op, newlines):
241 """Update the version value and add the 'npmsw://' url"""
242 value = origvalue.replace("version=" + data["version"], "version=${PV}")
243 value = value.replace("version=latest", "version=${PV}")
244 values = [line.strip() for line in value.strip('\n').splitlines()]
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600245 if "dependencies" in shrinkwrap:
246 values.append(url_recipe)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500247 return values, None, 4, False
248
249 (_, newlines) = bb.utils.edit_metadata(lines_before, ["SRC_URI"], _handle_srcuri)
250 lines_before[:] = [line.rstrip('\n') for line in newlines]
251
252 # In order to generate correct licence checksums in the recipe the
253 # dependencies have to be fetched again using the npmsw url
254 bb.note("Fetching npm dependencies ...")
255 bb.utils.remove(os.path.join(srctree, "node_modules"), recurse=True)
256 fetcher = bb.fetch2.Fetch([url_local], d)
257 fetcher.download()
258 fetcher.unpack(srctree)
259
260 bb.note("Handling licences ...")
261 (licfiles, packages) = self._handle_licenses(srctree, shrinkwrap_file, dev)
Andrew Geisslereff27472021-10-29 15:35:00 -0500262
263 def _guess_odd_license(licfiles):
264 import bb
265
266 md5sums = get_license_md5sums(d, linenumbers=True)
267
268 chksums = []
269 licenses = []
270 for licfile in licfiles:
271 f = os.path.join(srctree, licfile)
272 md5value = bb.utils.md5_file(f)
273 (license, beginline, endline, md5) = md5sums.get(md5value,
274 (None, "", "", ""))
275 if not license:
276 license = "Unknown"
277 logger.info("Please add the following line for '%s' to a "
278 "'lib/recipetool/licenses.csv' and replace `Unknown`, "
279 "`X`, `Y` and `MD5` with the license, begin line, "
280 "end line and partial MD5 checksum:\n" \
281 "%s,Unknown,X,Y,MD5" % (licfile, md5value))
282 chksums.append("file://%s%s%s;md5=%s" % (licfile,
283 ";beginline=%s" % (beginline) if beginline else "",
284 ";endline=%s" % (endline) if endline else "",
285 md5 if md5 else md5value))
286 licenses.append((license, licfile, md5value))
287 return (licenses, chksums)
288
289 (licenses, extravalues["LIC_FILES_CHKSUM"]) = _guess_odd_license(licfiles)
290 split_pkg_licenses([*licenses, *guess_license(srctree, d)], packages, lines_after)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500291
292 classes.append("npm")
293 handled.append("buildsystem")
294
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500295 return True
296
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500297def register_recipe_handlers(handlers):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500298 """Register the npm handler"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500299 handlers.append((NpmRecipeHandler(), 60))